/** * Provides a hook into the login page. * * @param object &$frm Form object. * @param object &$user User object. */ public function loginpage_hook(&$frm, &$user) { global $DB; if (empty($frm)) { $frm = data_submitted(); } if (empty($frm)) { return true; } $autoappend = get_config('auth_oidc', 'autoappend'); if (empty($autoappend)) { // If we're not doing autoappend, just let things flow naturally. return true; } $username = $frm->username; $password = $frm->password; $auth = 'oidc'; $existinguser = $DB->get_record('user', ['username' => $username]); if (!empty($existinguser)) { // We don't want to prevent access to existing accounts. return true; } $username .= $autoappend; $success = $this->user_login($username, $password); if ($success !== true) { // No o365 user, continue normally. return false; } $existinguser = $DB->get_record('user', ['username' => $username]); if (!empty($existinguser)) { $user = $existinguser; return true; } // The user is authenticated but user creation may be disabled. if (!empty($CFG->authpreventaccountcreation)) { $failurereason = AUTH_LOGIN_UNAUTHORISED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Unknown user, can not create new accounts: {$username} " . $_SERVER['HTTP_USER_AGENT']); return false; } $user = create_user_record($username, $password, $auth); return true; }
/** * Handle a login event. * * @param string $oidcuniqid A unique identifier for the user. * @param array $authparams Parameters receieved from the auth request. * @param array $tokenparams Parameters received from the token request. * @param \auth_oidc\jwt $idtoken A JWT object representing the received id_token. */ protected function handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken) { global $DB, $CFG; $tokenrec = $DB->get_record('auth_oidc_token', ['oidcuniqid' => $oidcuniqid]); if (!empty($tokenrec)) { $username = $tokenrec->username; $this->updatetoken($tokenrec->id, $authparams, $tokenparams); } else { // Use 'upn' if available for username (Azure-specific), or fall back to lower-case oidcuniqid. $username = $idtoken->claim('upn'); if (empty($username)) { $username = strtolower($oidcuniqid); } $matchedwith = $this->check_for_matched($username); if (!empty($matchedwith)) { $matchedwith->aadupn = $username; throw new \moodle_exception('errorusermatched', 'local_o365', null, $matchedwith); } $tokenrec = $this->createtoken($oidcuniqid, $username, $authparams, $tokenparams, $idtoken); } $existinguserparams = ['username' => $username, 'mnethostid' => $CFG->mnet_localhost_id]; if ($DB->record_exists('user', $existinguserparams) !== true) { // User does not exist. Create user if site allows, otherwise fail. if (empty($CFG->authpreventaccountcreation)) { $user = create_user_record($username, null, 'oidc'); } else { // Trigger login failed event. $failurereason = AUTH_LOGIN_NOUSER; $eventdata = ['other' => ['username' => $username, 'reason' => $failurereason]]; $event = \core\event\user_login_failed::create($eventdata); $event->trigger(); throw new \moodle_exception('errorauthloginfailednouser', 'auth_oidc'); } } $user = authenticate_user_login($username, null, true); if (empty($user)) { throw new \moodle_exception('errorauthloginfailednouser', 'auth_oidc'); } complete_user_login($user); return true; }
/** * Authenticates a user against the chosen authentication mechanism * * Given a username and password, this function looks them * up using the currently selected authentication mechanism, * and if the authentication is successful, it returns a * valid $user object from the 'user' table. * * Uses auth_ functions from the currently active auth module * * After authenticate_user_login() returns success, you will need to * log that the user has logged in, and call complete_user_login() to set * the session up. * * Note: this function works only with non-mnet accounts! * * @param string $username User's username (or also email if $CFG->authloginviaemail enabled) * @param string $password User's password * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists) * @return stdClass|false A {@link $USER} object or false if error */ function authenticate_user_login($username, $password, $ignorelockout = false, &$failurereason = null) { global $CFG, $DB; require_once "{$CFG->libdir}/authlib.php"; if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) { // we have found the user } else { if (!empty($CFG->authloginviaemail)) { if ($email = clean_param($username, PARAM_EMAIL)) { $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0"; $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email); $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2); if (count($users) === 1) { // Use email for login only if unique. $user = reset($users); $user = get_complete_user_data('id', $user->id); $username = $user->username; } unset($users); } } } $authsenabled = get_enabled_auth_plugins(); if ($user) { // Use manual if auth not set. $auth = empty($user->auth) ? 'manual' : $user->auth; if (in_array($user->auth, $authsenabled)) { $authplugin = get_auth_plugin($user->auth); $authplugin->pre_user_login_hook($user); } if (!empty($user->suspended)) { $failurereason = AUTH_LOGIN_SUSPENDED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Suspended Login: {$username} " . $_SERVER['HTTP_USER_AGENT']); return false; } if ($auth == 'nologin' or !is_enabled_auth($auth)) { // Legacy way to suspend user. $failurereason = AUTH_LOGIN_SUSPENDED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Disabled Login: {$username} " . $_SERVER['HTTP_USER_AGENT']); return false; } $auths = array($auth); } else { // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user(). if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) { $failurereason = AUTH_LOGIN_NOUSER; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Deleted Login: {$username} " . $_SERVER['HTTP_USER_AGENT']); return false; } // User does not exist. $auths = $authsenabled; $user = new stdClass(); $user->id = 0; } if ($ignorelockout) { // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA // or this function is called from a SSO script. } else { if ($user->id) { // Verify login lockout after other ways that may prevent user login. if (login_is_lockedout($user)) { $failurereason = AUTH_LOGIN_LOCKOUT; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Login lockout: {$username} " . $_SERVER['HTTP_USER_AGENT']); return false; } } else { // We can not lockout non-existing accounts. } } foreach ($auths as $auth) { $authplugin = get_auth_plugin($auth); // On auth fail fall through to the next plugin. if (!$authplugin->user_login($username, $password)) { continue; } // Successful authentication. if ($user->id) { // User already exists in database. if (empty($user->auth)) { // For some reason auth isn't set yet. $DB->set_field('user', 'auth', $auth, array('id' => $user->id)); $user->auth = $auth; } // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to // the current hash algorithm while we have access to the user's password. update_internal_user_password($user, $password); if ($authplugin->is_synchronised_with_external()) { // Update user record from external DB. $user = update_user_record_by_id($user->id); } } else { // The user is authenticated but user creation may be disabled. if (!empty($CFG->authpreventaccountcreation)) { $failurereason = AUTH_LOGIN_UNAUTHORISED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Unknown user, can not create new accounts: {$username} " . $_SERVER['HTTP_USER_AGENT']); return false; } else { $user = create_user_record($username, $password, $auth); } } $authplugin->sync_roles($user); foreach ($authsenabled as $hau) { $hauth = get_auth_plugin($hau); $hauth->user_authenticated_hook($user, $username, $password); } if (empty($user->id)) { $failurereason = AUTH_LOGIN_NOUSER; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); return false; } if (!empty($user->suspended)) { // Just in case some auth plugin suspended account. $failurereason = AUTH_LOGIN_SUSPENDED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Suspended Login: {$username} " . $_SERVER['HTTP_USER_AGENT']); return false; } login_attempt_valid($user); $failurereason = AUTH_LOGIN_OK; return $user; } // Failed if all the plugins have failed. if (debugging('', DEBUG_ALL)) { error_log('[client ' . getremoteaddr() . "] {$CFG->wwwroot} Failed Login: {$username} " . $_SERVER['HTTP_USER_AGENT']); } if ($user->id) { login_attempt_failed($user); $failurereason = AUTH_LOGIN_FAILED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); } else { $failurereason = AUTH_LOGIN_NOUSER; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); } return false; }