diff --git a/classes/adminsetting/auth_oidc_admin_setting_redirecturi.php b/classes/adminsetting/auth_oidc_admin_setting_redirecturi.php index 5763c8c..f2b5090 100644 --- a/classes/adminsetting/auth_oidc_admin_setting_redirecturi.php +++ b/classes/adminsetting/auth_oidc_admin_setting_redirecturi.php @@ -33,6 +33,8 @@ * Displays the redirect URI for easier config. */ class auth_oidc_admin_setting_redirecturi extends \admin_setting { + private $url; + /** * Constructor. * @@ -40,8 +42,9 @@ class auth_oidc_admin_setting_redirecturi extends \admin_setting { * @param $heading * @param $description */ - public function __construct($name, $heading, $description) { + public function __construct($name, $heading, $description, $url) { $this->nosave = true; + $this->url = $url; parent::__construct($name, $heading, $description, ''); } @@ -82,6 +85,7 @@ public function write_setting($data) { */ public function output_html($data, $query = '') { $redirecturl = utils::get_redirecturl(); + $redirecturl = $this->url; $html = \html_writer::tag('h5', $redirecturl); return format_admin_setting($this, $this->visiblename, $html, $this->description, true, '', null, $query); } diff --git a/classes/loginflow/authcode.php b/classes/loginflow/authcode.php index 221e018..32dbc59 100644 --- a/classes/loginflow/authcode.php +++ b/classes/loginflow/authcode.php @@ -26,7 +26,9 @@ namespace auth_oidc\loginflow; +use auth_oidc\jwt; use auth_oidc\utils; +use moodle_exception; defined('MOODLE_INTERNAL') || die(); @@ -88,7 +90,7 @@ protected function getoidcparam($name, $fallback = '') { $valclean = preg_replace('/[^A-Za-z0-9\_\-\.\+\/\=]/i', '', $val); if ($valclean !== $val) { utils::debug('Authorization error.', 'authcode::cleanoidcparam', $name); - throw new \moodle_exception('errorauthgeneral', 'auth_oidc'); + throw new moodle_exception('errorauthgeneral', 'auth_oidc'); } return $valclean; } @@ -179,6 +181,8 @@ public function initiateauthrequest($promptlogin = false, array $stateparams = a protected function handleauthresponse(array $authparams) { global $DB, $SESSION, $USER, $CFG; + $sid = optional_param('session_state', '', PARAM_TEXT); + if (!empty($authparams['error_description'])) { utils::debug('Authorization error.', 'authcode::handleauthresponse', $authparams); redirect($CFG->wwwroot, get_string('errorauthgeneral', 'auth_oidc'), null, \core\output\notification::NOTIFY_ERROR); @@ -186,18 +190,18 @@ protected function handleauthresponse(array $authparams) { if (!isset($authparams['code'])) { utils::debug('No auth code received.', 'authcode::handleauthresponse', $authparams); - throw new \moodle_exception('errorauthnoauthcode', 'auth_oidc'); + throw new moodle_exception('errorauthnoauthcode', 'auth_oidc'); } if (!isset($authparams['state'])) { utils::debug('No state received.', 'authcode::handleauthresponse', $authparams); - throw new \moodle_exception('errorauthunknownstate', 'auth_oidc'); + 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'); + throw new moodle_exception('errorauthunknownstate', 'auth_oidc'); } $orignonce = $staterec->nonce; $additionaldata = []; @@ -214,10 +218,10 @@ protected function handleauthresponse(array $authparams) { $client = $this->get_oidcclient(); $tokenparams = $client->tokenrequest($authparams['code']); if (!isset($tokenparams['id_token'])) { - throw new \moodle_exception('errorauthnoidtoken', 'auth_oidc'); + throw new moodle_exception('errorauthnoidtoken', 'auth_oidc'); } - // Decode and verify idtoken. + // Decode and verify ID token. [$oidcuniqid, $idtoken] = $this->process_idtoken($tokenparams['id_token'], $orignonce); // Check restrictions. @@ -225,7 +229,7 @@ protected function handleauthresponse(array $authparams) { if ($passed !== true && empty($additionaldata['ignorerestrictions'])) { $errstr = 'User prevented from logging in due to restrictions.'; utils::debug($errstr, 'handleauthresponse', $idtoken); - throw new \moodle_exception('errorrestricted', 'auth_oidc'); + throw new moodle_exception('errorrestricted', 'auth_oidc'); } // This is for setting the system API user. @@ -245,7 +249,6 @@ protected function handleauthresponse(array $authparams) { // Check if OIDC user is already migrated. $tokenrec = $DB->get_record('auth_oidc_token', ['oidcuniqid' => $oidcuniqid]); if (isloggedin() && !isguestuser() && (empty($tokenrec) || (isset($USER->auth) && $USER->auth !== 'oidc'))) { - // If user is already logged in and trying to link Microsoft 365 account or use it for OIDC. // Check if that Microsoft 365 account already exists in moodle. $userrec = $DB->count_records_sql('SELECT COUNT(*) @@ -260,7 +263,7 @@ protected function handleauthresponse(array $authparams) { } else if ($additionaldata['redirect'] == '/local/o365/ucp.php') { $redirect = $additionaldata['redirect'].'?action=connection&o365accountconnected=true'; } else { - throw new \moodle_exception('errorinvalidredirect_message', 'auth_oidc'); + throw new moodle_exception('errorinvalidredirect_message', 'auth_oidc'); } redirect(new \moodle_url($redirect)); } @@ -276,6 +279,9 @@ protected function handleauthresponse(array $authparams) { } else { // Otherwise it's a user logging in normally with OIDC. $this->handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken); + if ($USER->id && $DB->record_exists('auth_oidc_token', ['userid' => $USER->id])) { + $DB->set_field('auth_oidc_token', 'sid', $sid, ['userid' => $USER->id]); + } redirect(core_login_get_return_url()); } } @@ -284,9 +290,9 @@ protected function handleauthresponse(array $authparams) { * Handle a user migration event. * * @param string $oidcuniqid A unique identifier for the user. - * @param array $authparams Paramteres receieved from the auth request. + * @param array $authparams Parameters received 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. + * @param jwt $idtoken A JWT object representing the received id_token. * @param bool $connectiononly Whether to just connect the user (true), or to connect and change login method (false). */ protected function handlemigration($oidcuniqid, $authparams, $tokenparams, $idtoken, $connectiononly = false) { @@ -312,7 +318,7 @@ protected function handlemigration($oidcuniqid, $authparams, $tokenparams, $idto return true; } else { // OIDC user connected to user that is not us. Can't continue. - throw new \moodle_exception('errorauthuserconnectedtodifferent', 'auth_oidc'); + throw new moodle_exception('errorauthuserconnectedtodifferent', 'auth_oidc'); } } } @@ -331,7 +337,7 @@ protected function handlemigration($oidcuniqid, $authparams, $tokenparams, $idto $this->updatetoken($tokenrec->id, $authparams, $tokenparams); return true; } else { - throw new \moodle_exception('errorauthuseralreadyconnected', 'auth_oidc'); + throw new moodle_exception('errorauthuseralreadyconnected', 'auth_oidc'); } } @@ -418,18 +424,18 @@ protected function check_objects($oidcuniqid, $username) { * Handle a login event. * * @param string $oidcuniqid A unique identifier for the user. - * @param array $authparams Parameters receieved from the auth request. + * @param array $authparams Parameters received 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. + * @param jwt $idtoken A JWT object representing the received id_token. */ - protected function handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken) { + protected function handlelogin(string $oidcuniqid, array $authparams, array $tokenparams, jwt $idtoken) { global $DB, $CFG; $tokenrec = $DB->get_record('auth_oidc_token', ['oidcuniqid' => $oidcuniqid]); // Do not continue if auth plugin is not enabled. if (!is_enabled_auth('oidc')) { - throw new \moodle_exception('erroroidcnotenabled', 'auth_oidc', null, null, '1'); + throw new moodle_exception('erroroidcnotenabled', 'auth_oidc', null, null, '1'); } if (!empty($tokenrec)) { @@ -466,10 +472,9 @@ protected function handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken) complete_user_login($user); } else { // There was a problem in authenticate_user_login. - throw new \moodle_exception('errorauthgeneral', 'auth_oidc', null, null, '2'); + throw new moodle_exception('errorauthgeneral', 'auth_oidc', null, null, '2'); } - return true; } else { /* No existing token, user not connected. Possibilities: - Matched user. @@ -501,7 +506,7 @@ protected function handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken) if (!empty($matchedwith)) { if ($matchedwith->auth != 'oidc') { $matchedwith->aadupn = $username; - throw new \moodle_exception('errorusermatched', 'auth_oidc', null, $matchedwith); + throw new moodle_exception('errorusermatched', 'auth_oidc', null, $matchedwith); } } $username = trim(\core_text::strtolower($username)); @@ -518,7 +523,7 @@ protected function handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken) $eventdata = ['other' => ['username' => $username, 'reason' => $failurereason]]; $event = \core\event\user_login_failed::create($eventdata); $event->trigger(); - throw new \moodle_exception('errorauthloginfailednouser', 'auth_oidc', null, null, '1'); + throw new moodle_exception('errorauthloginfailednouser', 'auth_oidc', null, null, '1'); } } @@ -543,7 +548,7 @@ protected function handlelogin($oidcuniqid, $authparams, $tokenparams, $idtoken) redirect($CFG->wwwroot, get_string('errorauthgeneral', 'auth_oidc'), null, \core\output\notification::NOTIFY_ERROR); } - return true; } + return true; } } diff --git a/classes/oidcclient.php b/classes/oidcclient.php index 15058fc..2ed18be 100644 --- a/classes/oidcclient.php +++ b/classes/oidcclient.php @@ -219,7 +219,6 @@ protected function getnewstate($nonce, array $stateparams = array()) { * @param array $extraparams Additional parameters to send with the OIDC request. */ public function authrequest($promptlogin = false, array $stateparams = array(), array $extraparams = array()) { - global $DB; if (empty($this->clientid)) { throw new \moodle_exception('erroroidcclientnocreds', 'auth_oidc'); } diff --git a/classes/utils.php b/classes/utils.php index 715fddd..9f2403b 100644 --- a/classes/utils.php +++ b/classes/utils.php @@ -133,8 +133,17 @@ public static function debug($message, $where = '', $debugdata = null) { * @return string The redirect URL. */ public static function get_redirecturl() { - global $CFG; - $wwwroot = (!empty($CFG->loginhttps)) ? str_replace('http://', 'https://', $CFG->wwwroot) : $CFG->wwwroot; - return $wwwroot.'/auth/oidc/'; + $redirecturl = new \moodle_url('/auth/oidc/'); + return $redirecturl->out(false); + } + + /** + * Get the front channel logout URL that should be set in the identity provider. + * + * @return string The redirect URL. + */ + public static function get_frontchannellogouturl() { + $logouturl = new \moodle_url('/auth/oidc/logout.php'); + return $logouturl->out(false); } } diff --git a/db/install.xml b/db/install.xml index d95815c..e2c2012 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -49,6 +49,7 @@ + @@ -60,4 +61,4 @@ - \ No newline at end of file + diff --git a/db/upgrade.php b/db/upgrade.php index 8a97ff5..281ce5c 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -312,5 +312,19 @@ function xmldb_auth_oidc_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2021051701, 'auth', 'oidc'); } + if ($oldversion < 2021051721) { + // Define field sid to be added to auth_oidc_token. + $table = new xmldb_table('auth_oidc_token'); + $field = new xmldb_field('sid', XMLDB_TYPE_CHAR, '36', null, null, null, null, 'idtoken'); + + // Conditionally launch add field sid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Oidc savepoint reached. + upgrade_plugin_savepoint(true, 2021051721, 'auth', 'oidc'); + } + return true; } diff --git a/lang/en/auth_oidc.php b/lang/en/auth_oidc.php index 3b44113..f00e255 100644 --- a/lang/en/auth_oidc.php +++ b/lang/en/auth_oidc.php @@ -87,12 +87,14 @@ $string['cfg_userrestrictions_desc'] = 'Only allow users to log in that meet certain restrictions.
How to use user restrictions: '; $string['cfg_userrestrictionscasesensitive_key'] = 'User Restrictions Case Sensitive'; $string['cfg_userrestrictioncasesensitive_desc'] = 'This controls if the "/i" option in regular expression is used in the user restriction match.
If enabled, all user restriction checks will be performed as with case sensitive. Note if this is disabled, any patterns on letter cases will be ignored.'; -$string['cfg_signoffintegration_key'] = 'Single sign off'; -$string['cfg_signoffintegration_desc'] = 'When connecting to Azure AD, if this option is enabled, when a Moodle user using the OpenID Connect authentication method signs off from Moodle, Moodle will attempt to log the user off from Microsoft 365 as well. - -Note the URL of Moodle site ({$a}) needs to be added as a redirect URI in the Azure app created for Moodle Microsoft 365 integration.'; +$string['cfg_signoffintegration_key'] = 'Single sign out'; +$string['cfg_signoffintegration_desc'] = 'If the option is enabled, when a Moodle user connected to the configured IdP logs out of Moodle, the integration will trigger a request at the logout endpiont below, attempting to log the user off from IdP as well.
+Note for integration with Microsoft Azure AD, the URL of Moodle site ({$a}) needs to be added as a redirect URI in the Azure app created for Moodle and Microsoft 365 integration.'; $string['cfg_logoutendpoint_key'] = 'Logout Endpoint'; $string['cfg_logoutendpoint_desc'] = 'The URI of the logout endpoint from your identity provider to use.'; +$string['cfg_frontchannellogouturl_key'] = 'Front-channel Logout URL'; +$string['cfg_frontchannellogouturl_desc'] = 'This is the URL that your IdP needs to trigger when it tries to log users out of Moodle.
+For Microsoft Azure AD, the setting is called "Front-channel logout URL" and is configurable in the Azure app.'; $string['cfg_tools'] = 'Tools'; $string['cfg_cleanupoidctokens_key'] = 'Cleanup OpenID Connect Tokens'; $string['cfg_cleanupoidctokens_desc'] = 'If your users are experiencing problems logging in using their Microsoft 365 account, trying cleaning up OpenID Connect tokens. This removes stray and incomplete tokens that can cause errors. WARNING: This may interrupt logins in-process, so it\'s best to do this during downtime.'; diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..57903fb --- /dev/null +++ b/logout.php @@ -0,0 +1,47 @@ +. + +/** + * Single Sign Out end point. + * + * @package auth_oidc + * @author Lai Wei + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright (C) 2014 onwards Microsoft, Inc. (http://microsoft.com/) + */ + +require_once(__DIR__ . '/../../config.php'); + +$PAGE->set_url('/auth/oidc/logout.php'); +$PAGE->set_context(context_system::instance()); + +$sid = optional_param('sid', '', PARAM_TEXT); + +if ($sid) { + if ($authoidctokenrecord = $DB->get_record('auth_oidc_token', ['sid' => $sid])) { + if ($authoidctokenrecord->userid == $USER->id) { + $authsequence = get_enabled_auth_plugins(); // auths, in sequence + foreach($authsequence as $authname) { + $authplugin = get_auth_plugin($authname); + $authplugin->logoutpage_hook(); + } + + require_logout(); + } + } +} + +die(); diff --git a/settings.php b/settings.php index 5432afe..8f0f2ad 100644 --- a/settings.php +++ b/settings.php @@ -30,6 +30,7 @@ use auth_oidc\adminsetting\auth_oidc_admin_setting_loginflow; use auth_oidc\adminsetting\auth_oidc_admin_setting_redirecturi; use auth_oidc\adminsetting\auth_oidc_admin_setting_label; +use auth_oidc\utils; require_once($CFG->dirroot . '/auth/oidc/lib.php'); @@ -68,7 +69,7 @@ $configkey = new lang_string('cfg_redirecturi_key', 'auth_oidc'); $configdesc = new lang_string('cfg_redirecturi_desc', 'auth_oidc'); -$settings->add(new auth_oidc_admin_setting_redirecturi('auth_oidc/redirecturi', $configkey, $configdesc)); +$settings->add(new auth_oidc_admin_setting_redirecturi('auth_oidc/redirecturi', $configkey, $configdesc, utils::get_redirecturl())); $configkey = new lang_string('cfg_forceredirect_key', 'auth_oidc'); $configdesc = new lang_string('cfg_forceredirect_desc', 'auth_oidc'); @@ -108,6 +109,11 @@ $configdefault = 'https://login.microsoftonline.com/common/oauth2/logout'; $settings->add(new admin_setting_configtext('auth_oidc/logouturi', $configkey, $configdesc, $configdefault, PARAM_TEXT)); +$configkey = new lang_string('cfg_frontchannellogouturl_key', 'auth_oidc'); +$configdesc = new lang_string('cfg_frontchannellogouturl_desc', 'auth_oidc'); +$settings->add(new auth_oidc_admin_setting_redirecturi('auth_oidc/logoutendpoint', $configkey, $configdesc, + utils::get_frontchannellogouturl())); + $label = new lang_string('cfg_debugmode_key', 'auth_oidc'); $desc = new lang_string('cfg_debugmode_desc', 'auth_oidc'); $settings->add(new \admin_setting_configcheckbox('auth_oidc/debugmode', $label, $desc, '0')); diff --git a/version.php b/version.php index 796a2f3..9c77102 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2021051720; +$plugin->version = 2021051721; $plugin->requires = 2021051700; $plugin->release = '3.11.3'; $plugin->component = 'auth_oidc';