/** * Process an authorization request. * * Operations: * - Auto creates users. * - Sets up user object for linked accounts. * * @param string $oidcuniqid The OIDC unique identifier received. * @param array $tokenparams Received token parameters. * @param \auth_oidc\jwt $idtoken Received id token. * @return bool Success/Failure. */ public function request_user_authorise($oidcuniqid, $tokenparams, $idtoken) { global $USER, $SESSION; $this->must_be_ready(); $username = $oidcuniqid; $email = $idtoken->claim('email'); $firstname = $idtoken->claim('given_name'); $lastname = $idtoken->claim('family_name'); // Office 365 uses "upn". $upn = $idtoken->claim('upn'); if (!empty($upn)) { $username = $upn; $email = $upn; } $create = false; try { $user = new \User(); $user->find_by_instanceid_username($this->instanceid, $username, true); if ($user->get('suspendedcusr')) { die_info(get_string('accountsuspended', 'mahara', strftime(get_string('strftimedaydate'), $user->get('suspendedctime')), $user->get('suspendedreason'))); } } catch (\AuthUnknownUserException $e) { if ($this->can_auto_create_users() === true) { $institution = new \Institution($this->institution); if ($institution->isFull()) { throw new \XmlrpcClientException('OpenID Connect login attempt failed because the institution is full.'); } $user = new \User(); $create = true; } else { return false; } } if ($create === true) { $user->passwordchange = 0; $user->active = 1; $user->deleted = 0; $user->expiry = null; $user->expirymailsent = 0; $user->lastlogin = time(); $user->firstname = $firstname; $user->lastname = $lastname; $user->email = $email; $user->authinstance = $this->instanceid; db_begin(); $user->username = get_new_username($username); $user->id = create_user($user, array(), $this->institution, $this, $username); $userobj = $user->to_stdclass(); $userarray = (array) $userobj; db_commit(); $user = new User(); $user->find_by_id($userobj->id); } $user->commit(); $USER->reanimate($user->id, $this->instanceid); $SESSION->set('authinstance', $this->instanceid); return true; }
/** * Return an XHTML string for the setting * @return string Returns an XHTML string */ public function output_html($data, $query = '') { $tokens = get_config('local_o365', 'systemtokens'); $setuser = ''; if (!empty($tokens)) { $tokens = unserialize($tokens); if (isset($tokens['idtoken'])) { try { $idtoken = \auth_oidc\jwt::instance_from_encoded($tokens['idtoken']); $setuser = $idtoken->claim('upn'); } catch (\Exception $e) { // There is a check below for an empty $setuser. } } } $settinghtml = '<input type="hidden" id="' . $this->get_id() . '" name="' . $this->get_full_name() . '" value="0" />'; $setuserurl = new \moodle_url('/local/o365/acp.php', ['mode' => 'setsystemuser']); if (!empty($setuser)) { $settinghtml .= get_string('settings_systemapiuser_userset', 'local_o365', $setuser) . ' '; $settinghtml .= \html_writer::link($setuserurl, get_string('settings_systemapiuser_change', 'local_o365')); } else { $settinghtml .= get_string('settings_systemapiuser_usernotset', 'local_o365') . ' '; $settinghtml .= \html_writer::link($setuserurl, get_string('settings_systemapiuser_setuser', 'local_o365')); } return format_admin_setting($this, $this->visiblename, $settinghtml, $this->description); }
/** * Test decode. * * @dataProvider dataprovider_decode */ public function test_decode($encodedjwt, $expectedresult, $expectedexception) { if (!empty($expectedexception)) { $this->setExpectedException($expectedexception[0], $expectedexception[1]); } $actualresult = \auth_oidc\jwt::decode($encodedjwt); $this->assertEquals($expectedresult, $actualresult); }
/** * Detect the proper auth instance based on received user information. * * @param \auth_oidc\jwt $idtoken JWT ID Token. * @return int|null The auth instance ID if found, or null if none found. */ protected function detect_auth_instance($idtoken) { // Get auth instance. $sql = 'SELECT ai.id as instanceid, i.priority as institutionpriority FROM {auth_instance} ai JOIN {institution} i ON i.name = ai.institution WHERE ai.authname = \'oidc\' ORDER BY i.priority DESC, ai.priority ASC'; $instances = get_records_sql_array($sql); $catchalls = array(); $instanceid = null; foreach ($instances as $instance) { $reqattr = get_config_plugin_instance('auth', $instance->instanceid, 'institutionattribute'); $reqval = get_config_plugin_instance('auth', $instance->instanceid, 'institutionvalue'); if (empty($reqattr) || empty($reqval)) { $catchalls[$instance->institutionpriority][] = $instance; } else { // Check if we received specified attribute. $userattrval = $idtoken->claim($reqattr); if (!empty($userattrval)) { // Match value. if (preg_match('#' . trim($reqval) . '#', $userattrval)) { $instanceid = $instance->instanceid; break; } } } } // If no match on attribute, get the instance id of the first catchall by priority. if (empty($instanceid)) { foreach ($catchalls as $priority => $instances) { foreach ($instances as $instance) { $instanceid = $instance->instanceid; break; } break; } } return $instanceid; }
/** * 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; }
/** * Return an XHTML string for the setting * @return string Returns an XHTML string */ public function output_html($data, $query = '') { global $OUTPUT; $tokens = get_config('local_o365', 'systemtokens'); $setuser = ''; if (!empty($tokens)) { $tokens = unserialize($tokens); if (isset($tokens['idtoken'])) { try { $idtoken = \auth_oidc\jwt::instance_from_encoded($tokens['idtoken']); $setuser = $idtoken->claim('upn'); } catch (\Exception $e) { // There is a check below for an empty $setuser. } } } $settinghtml = '<input type="hidden" id="' . $this->get_id() . '" name="' . $this->get_full_name() . '" value="0" />'; $setuserurl = new \moodle_url('/local/o365/acp.php', ['mode' => 'setsystemuser']); if (!empty($setuser)) { $message = \html_writer::tag('span', get_string('settings_systemapiuser_userset', 'local_o365', $setuser)) . ' '; $linkstr = get_string('settings_systemapiuser_change', 'local_o365'); $message .= \html_writer::link($setuserurl, $linkstr, ['class' => 'btn', 'style' => 'margin-left: 0.5rem']); $messageattrs = ['class' => 'local_o365_statusmessage alert-success']; $icon = $OUTPUT->pix_icon('t/check', 'success', 'moodle'); $settinghtml .= \html_writer::tag('div', $icon . $message, $messageattrs); } else { $message = \html_writer::tag('span', get_string('settings_systemapiuser_usernotset', 'local_o365')) . ' '; $linkstr = get_string('settings_systemapiuser_setuser', 'local_o365'); $message .= \html_writer::link($setuserurl, $linkstr, ['class' => 'btn', 'style' => 'margin-left: 0.5rem']); $messageattrs = ['class' => 'local_o365_statusmessage alert-info']; $icon = $OUTPUT->pix_icon('i/warning', 'warning', 'moodle'); $settinghtml .= \html_writer::tag('div', $icon . $message, $messageattrs); } return format_admin_setting($this, $this->visiblename, $settinghtml, $this->description); }
/** * Create a token for a user, thus linking a Moodle user to an OpenID Connect user. * * @param string $oidcuniqid A unique identifier for the user. * @param array $username The username of the Moodle user to link to. * @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. * @return \stdClass The created token database record. */ protected function createtoken($oidcuniqid, $username, $authparams, $tokenparams, \auth_oidc\jwt $idtoken) { global $DB; // Determine remote username. Use 'upn' if available (Azure-specific), or fall back to standard 'sub'. $oidcusername = $idtoken->claim('upn'); if (empty($oidcusername)) { $oidcusername = $idtoken->claim('sub'); } // We should not fail here (idtoken was verified earlier to at least contain 'sub', but just in case...). if (empty($oidcusername)) { throw new \moodle_exception('errorauthinvalididtoken', 'auth_oidc'); } $tokenrec = new \stdClass(); $tokenrec->oidcuniqid = $oidcuniqid; $tokenrec->username = $username; $tokenrec->oidcusername = $oidcusername; $tokenrec->scope = $tokenparams['scope']; $tokenrec->resource = $tokenparams['resource']; $tokenrec->authcode = $authparams['code']; $tokenrec->token = $tokenparams['access_token']; $tokenrec->expiry = $tokenparams['expires_on']; $tokenrec->refreshtoken = $tokenparams['refresh_token']; $tokenrec->idtoken = $tokenparams['id_token']; $tokenrec->id = $DB->insert_record('auth_oidc_token', $tokenrec); return $tokenrec; }
/** * Update plugin. * * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_oidc_upgrade($oldversion) { global $DB; $dbman = $DB->get_manager(); $result = true; if ($result && $oldversion < 2014111703) { // Lengthen field. $table = new xmldb_table('auth_oidc_token'); $field = new xmldb_field('scope', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'username'); $dbman->change_field_type($table, $field); upgrade_plugin_savepoint($result, '2014111703', 'auth', 'oidc'); } if ($result && $oldversion < 2015012702) { $table = new xmldb_table('auth_oidc_state'); $field = new xmldb_field('additionaldata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'timecreated'); if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } upgrade_plugin_savepoint($result, '2015012702', 'auth', 'oidc'); } if ($result && $oldversion < 2015012703) { $table = new xmldb_table('auth_oidc_token'); $field = new xmldb_field('oidcusername', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'username'); if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } upgrade_plugin_savepoint($result, '2015012703', 'auth', 'oidc'); } if ($result && $oldversion < 2015012704) { // Update OIDC users. $sql = 'SELECT u.id as userid, u.username as username, tok.id as tokenid, tok.oidcuniqid as oidcuniqid, tok.idtoken as idtoken, tok.oidcusername as oidcusername FROM {auth_oidc_token} tok JOIN {user} u ON u.username = tok.username WHERE u.auth = ? AND deleted = 0'; $params = ['oidc']; $userstoupdate = $DB->get_recordset_sql($sql, $params); foreach ($userstoupdate as $user) { if (empty($user->idtoken)) { continue; } try { // Decode idtoken and determine oidc username. $idtoken = \auth_oidc\jwt::instance_from_encoded($user->idtoken); $oidcusername = $idtoken->claim('upn'); if (empty($oidcusername)) { $oidcusername = $idtoken->claim('sub'); } // Populate token oidcusername. if (empty($user->oidcusername)) { $updatedtoken = new \stdClass(); $updatedtoken->id = $user->tokenid; $updatedtoken->oidcusername = $oidcusername; $DB->update_record('auth_oidc_token', $updatedtoken); } // Update user username (if applicable), so user can use rocreds loginflow. if ($user->username == strtolower($user->oidcuniqid)) { // Old username, update to upn/sub. if ($oidcusername != $user->username) { // Update username. $updateduser = new \stdClass(); $updateduser->id = $user->userid; $updateduser->username = $oidcusername; $DB->update_record('user', $updateduser); $updatedtoken = new \stdClass(); $updatedtoken->id = $user->tokenid; $updatedtoken->username = $oidcusername; $DB->update_record('auth_oidc_token', $updatedtoken); } } } catch (\Exception $e) { continue; } } upgrade_plugin_savepoint($result, '2015012704', 'auth', 'oidc'); } if ($result && $oldversion < 2015012707) { if (!$dbman->table_exists('auth_oidc_prevlogin')) { $dbman->install_one_table_from_xmldb_file(__DIR__ . '/install.xml', 'auth_oidc_prevlogin'); } upgrade_plugin_savepoint($result, '2015012707', 'auth', 'oidc'); } if ($result && $oldversion < 2015012710) { // Lengthen field. $table = new xmldb_table('auth_oidc_token'); $field = new xmldb_field('scope', XMLDB_TYPE_TEXT, null, null, null, null, null, 'oidcusername'); $dbman->change_field_type($table, $field); upgrade_plugin_savepoint($result, '2015012710', 'auth', 'oidc'); } return $result; }
/** * Process an idtoken, extract uniqid and construct jwt object. * * @param string $idtoken Encoded id token. * @param string $orignonce Original nonce to validate received nonce against. * @return array List of oidcuniqid and constructed idtoken jwt. */ protected function process_idtoken($idtoken, $orignonce = '') { // Decode and verify idtoken. $idtoken = \auth_oidc\jwt::instance_from_encoded($idtoken); $sub = $idtoken->claim('sub'); if (empty($sub)) { throw new \AuthInstanceException(get_string('errorauthinvalididtoken', 'auth.oidc')); } $receivednonce = $idtoken->claim('nonce'); if (!empty($orignonce) && (empty($receivednonce) || $receivednonce !== $orignonce)) { throw new \AuthInstanceException(get_string('errorauthinvalididtoken', 'auth.oidc')); } // Use 'oid' if available (Azure-specific), or fall back to standard "sub" claim. $oidcuniqid = $idtoken->claim('oid'); if (empty($oidcuniqid)) { $oidcuniqid = $idtoken->claim('sub'); } return array($oidcuniqid, $idtoken); }