/** * Process an OIDC JSON response. * * @param string $response The received JSON. * @return array The parsed JSON. */ public static function process_json_response($response, array $expectedstructure = array()) { $backtrace = debug_backtrace(0); $callingclass = isset($backtrace[1]['class']) ? $backtrace[1]['class'] : '?'; $callingfunc = isset($backtrace[1]['function']) ? $backtrace[1]['function'] : '?'; $callingline = isset($backtrace[0]['line']) ? $backtrace[0]['line'] : '?'; $caller = $callingclass . '::' . $callingfunc . ':' . $callingline; $result = @json_decode($response, true); if (empty($result) || !is_array($result)) { \auth_oidc\utils::debug('Bad response received', $caller, $response); throw new \moodle_exception('erroroidccall', 'auth_oidc'); } if (isset($result['error'])) { $errmsg = 'Error response received.'; \auth_oidc\utils::debug($errmsg, $caller, $result); if (isset($result['error_description'])) { throw new \moodle_exception('erroroidccall_message', 'auth_oidc', '', $result['error_description']); } else { throw new \moodle_exception('erroroidccall', 'auth_oidc'); } } foreach ($expectedstructure as $key => $val) { if (!isset($result[$key])) { $errmsg = 'Invalid structure received. No "' . $key . '"'; \auth_oidc\utils::debug($errmsg, $caller, $result); throw new \moodle_exception('erroroidccall', 'auth_oidc'); } if ($val !== null && $result[$key] !== $val) { $strreceivedval = \auth_oidc\utils::tostring($result[$key]); $strval = \auth_oidc\utils::tostring($val); $errmsg = 'Invalid structure received. Invalid "' . $key . '". Received "' . $strreceivedval . '", expected "' . $strval . '"'; \auth_oidc\utils::debug($errmsg, $caller, $result); throw new \moodle_exception('erroroidccall', 'auth_oidc'); } } return $result; }
/** * Handle an authorization request response received from the configured OP. * * @param array $authparams Received parameters. */ protected function handleauthresponse(array $authparams) { global $DB, $CFG, $SESSION, $STATEADDITIONALDATA, $USER; if (!isset($authparams['code'])) { \auth_oidc\utils::debug('No auth code received.', 'authcode::handleauthresponse', $authparams); throw new \moodle_exception('errorauthnoauthcode', 'auth_oidc'); } if (!isset($authparams['state'])) { \auth_oidc\utils::debug('No state received.', 'authcode::handleauthresponse', $authparams); throw new \moodle_exception('errorauthunknownstate', 'auth_oidc'); } // Validate and expire state. $staterec = $DB->get_record('auth_oidc_state', ['state' => $authparams['state']]); if (empty($staterec)) { throw new \moodle_exception('errorauthunknownstate', 'auth_oidc'); } $orignonce = $staterec->nonce; $additionaldata = []; if (!empty($staterec->additionaldata)) { $additionaldata = @unserialize($staterec->additionaldata); if (!is_array($additionaldata)) { $additionaldata = []; } } $STATEADDITIONALDATA = $additionaldata; $DB->delete_records('auth_oidc_state', ['id' => $staterec->id]); // Get token from auth code. $client = $this->get_oidcclient(); $tokenparams = $client->tokenrequest($authparams['code']); if (!isset($tokenparams['id_token'])) { throw new \moodle_exception('errorauthnoidtoken', 'auth_oidc'); } // Decode and verify idtoken. list($oidcuniqid, $idtoken) = $this->process_idtoken($tokenparams['id_token'], $orignonce); // Check restrictions. $passed = $this->checkrestrictions($idtoken); if ($passed !== true) { $errstr = 'User prevented from logging in due to restrictions.'; \auth_oidc\utils::debug($errstr, 'handleauthresponse', $idtoken); throw new \moodle_exception('errorrestricted', 'auth_oidc'); } // This is for setting the system API user. if (isset($SESSION->auth_oidc_justevent)) { unset($SESSION->auth_oidc_justevent); $eventdata = ['other' => ['authparams' => $authparams, 'tokenparams' => $tokenparams]]; $event = \auth_oidc\event\user_authed::create($eventdata); $event->trigger(); return true; } // Check if OIDC user is already migrated. $tokenrec = $DB->get_record('auth_oidc_token', ['oidcuniqid' => $oidcuniqid]); if (isloggedin() === true && (empty($tokenrec) || isset($USER->auth) && $USER->auth !== 'oidc')) { // If the user is already logged in we can treat this as a "migration" - a user switching to OIDC. $connectiononly = false; if (isset($SESSION->auth_oidc_connectiononly)) { $connectiononly = true; unset($SESSION->auth_oidc_connectiononly); } if (isset($STATEADDITIONALDATA['connectiononly']) && $STATEADDITIONALDATA['connectiononly'] === true) { $connectiononly = true; } $this->handlemigration($oidcuniqid, $authparams, $tokenparams, $idtoken, $connectiononly); $redirect = !empty($additionaldata['redirect']) ? $additionaldata['redirect'] : '/auth/oidc/ucp.php'; redirect(new \moodle_url($redirect)); } else { // Otherwise it's a user logging in normally with OIDC. $this->handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken); redirect(core_login_get_return_url()); } }
/** * Exchange an authorization code for an access token. * * @param string $tokenendpoint The token endpoint URI. * @param string $code An authorization code. * @return array Received parameters. */ public function tokenrequest($code) { if (empty($this->endpoints['token'])) { throw new \moodle_exception('erroroidcclientnotokenendpoint', 'auth_oidc'); } $params = ['client_id' => $this->clientid, 'client_secret' => $this->clientsecret, 'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => $this->redirecturi]; $returned = $this->httpclient->post($this->endpoints['token'], $params); return \auth_oidc\utils::process_json_response($returned, ['id_token' => null]); }
/** * Check user restrictions, if present. * * This check will return false if there are restrictions in place that the user did not meet, otherwise it will return * true. If there are no restrictions in place, this will return true. * * @param \auth_oidc\jwt $idtoken The ID token of the user who is trying to log in. * @return bool Whether the restriction check passed. */ protected function checkrestrictions(\auth_oidc\jwt $idtoken) { $restrictions = isset($this->config->userrestrictions) ? trim($this->config->userrestrictions) : ''; $hasrestrictions = false; $userpassed = false; if ($restrictions !== '') { $restrictions = explode("\n", $restrictions); // Match "UPN" (Azure-specific) if available, otherwise match oidc-standard "sub". $tomatch = $idtoken->claim('upn'); if (empty($tomatch)) { $tomatch = $idtoken->claim('sub'); } foreach ($restrictions as $restriction) { $restriction = trim($restriction); if ($restriction !== '') { $hasrestrictions = true; ob_start(); try { $count = @preg_match('/' . $restriction . '/', $tomatch, $matches); if (!empty($count)) { $userpassed = true; break; } } catch (\Exception $e) { $debugdata = ['exception' => $e, 'restriction' => $restriction, 'tomatch' => $tomatch]; \auth_oidc\utils::debug('Error running user restrictions.', 'handleauthresponse', $debugdata); } $contents = ob_get_contents(); ob_end_clean(); if (!empty($contents)) { $debugdata = ['contents' => $contents, 'restriction' => $restriction, 'tomatch' => $tomatch]; \auth_oidc\utils::debug('Output while running user restrictions.', 'handleauthresponse', $debugdata); } } } } return $hasrestrictions === true && $userpassed !== true ? false : true; }
/** * This is the primary method that is used by the authenticate_user_login() function in moodlelib.php. * * @param string $username The username (with system magic quotes) * @param string $password The password (with system magic quotes) * @return bool Authentication success or failure. */ public function user_login($username, $password = null) { global $CFG, $DB; $client = $this->get_oidcclient(); $authparams = ['code' => '']; $oidcusername = $username; $oidctoken = $DB->get_records('auth_oidc_token', ['username' => $username]); if (!empty($oidctoken)) { $oidctoken = array_shift($oidctoken); if (!empty($oidctoken) && !empty($oidctoken->oidcusername)) { $oidcusername = $oidctoken->oidcusername; } } // Make request. $tokenparams = $client->rocredsrequest($oidcusername, $password); if (!empty($tokenparams) && isset($tokenparams['token_type']) && $tokenparams['token_type'] === 'Bearer') { list($oidcuniqid, $idtoken) = $this->process_idtoken($tokenparams['id_token']); // Check restrictions. $passed = $this->checkrestrictions($idtoken); if ($passed !== true) { $errstr = 'User prevented from logging in due to restrictions.'; \auth_oidc\utils::debug($errstr, 'handleauthresponse', $idtoken); return false; } $tokenrec = $DB->get_record('auth_oidc_token', ['oidcuniqid' => $oidcuniqid]); if (!empty($tokenrec)) { $this->updatetoken($tokenrec->id, $authparams, $tokenparams); } else { $tokenrec = $this->createtoken($oidcuniqid, $username, $authparams, $tokenparams, $idtoken); } return true; } return false; }