From 21afa2e677db6684142edc85397f921ed30cbf42 Mon Sep 17 00:00:00 2001 From: Lai Wei Date: Mon, 13 May 2024 09:59:10 +0100 Subject: [PATCH] Silent SSO login implementation --- auth.php | 23 +++++++++++++++++------ classes/loginflow/authcode.php | 31 +++++++++++++++++++++++++++---- classes/oidcclient.php | 20 +++++++++++++++++--- lang/en/auth_oidc.php | 16 ++++++++++++++++ settings.php | 6 ++++++ 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/auth.php b/auth.php index 2c9f107..3d96dfe 100644 --- a/auth.php +++ b/auth.php @@ -121,7 +121,8 @@ public function loginpage_hook() { * @return bool If this returns true then redirect */ public function should_login_redirect() { - global $SESSION; + global $CFG, $SESSION; + $oidc = optional_param('oidc', null, PARAM_BOOL); // Also support noredirect param - used by other auth plugins. $noredirect = optional_param('noredirect', 0, PARAM_BOOL); @@ -137,13 +138,22 @@ public function should_login_redirect() { } // Check whether we've skipped the login page already. - // This is here because loginpage_hook is called again during form - // submission (all of login.php is processed) and ?oidc=off is not - // preserved forcing us to the IdP. + // This is here because loginpage_hook is called again during form submission (all of login.php is processed) and + // ?oidc=off is not preserved forcing us to the IdP. // - // This isn't needed when duallogin is on because $oidc will default to 0 - // and duallogin is not part of the request. + // This isn't needed when duallogin is on because $oidc will default to 0 and duallogin is not part of the request. if ((isset($SESSION->oidc) && $SESSION->oidc == 0)) { + if (!isset($SESSION->silent_login_mode)) { + return false; + } + } + + // If the user is redirectred to the login page immediately after logging out, don't redirect. + $silentloginmodesetting = get_config('auth_oidc', 'silentloginmode'); + $forceredirectsetting = get_config('auth_oidc', 'forceredirect'); + $forceloginsetting = get_config('core', 'forcelogin'); + if ($silentloginmodesetting && $forceredirectsetting && $forceloginsetting && isset($_SERVER['HTTP_REFERER']) && + strpos($_SERVER['HTTP_REFERER'], $CFG->wwwroot) !== false) { return false; } @@ -156,6 +166,7 @@ public function should_login_redirect() { if (isset($SESSION->oidc)) { unset($SESSION->oidc); } + return true; } diff --git a/classes/loginflow/authcode.php b/classes/loginflow/authcode.php index 6c122e9..6ad7eb2 100644 --- a/classes/loginflow/authcode.php +++ b/classes/loginflow/authcode.php @@ -108,6 +108,27 @@ protected function getoidcparam($name, $fallback = '') { public function handleredirect() { global $CFG, $SESSION; + $error = optional_param('error', '', PARAM_TEXT); + $errordescription = optional_param('error_description', '', PARAM_TEXT); + $silentloginmode = get_config('auth_oidc', 'silentloginmode'); + $selectaccount = false; + if ($silentloginmode) { + if ($error == 'login_required') { + // If silent login mode is enabled and the error is 'login_required', redirect to the login page. + $loginpageurl = new moodle_url('/login/index.php', ['noredirect' => 1]); + redirect($loginpageurl); + die(); + } else if ($error == 'interaction_required') { + if (strpos($errordescription, 'multiple user identities') !== false) { + $selectaccount = true; + } else { + $loginpageurl = new moodle_url('/login/index.php', ['noredirect' => 1]); + redirect($loginpageurl); + die(); + } + } + } + if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT_IDENTITY_PLATFORM) { $adminconsent = optional_param('admin_consent', '', PARAM_TEXT); if ($adminconsent) { @@ -127,7 +148,7 @@ public function handleredirect() { $promptlogin = (bool)optional_param('promptlogin', 0, PARAM_BOOL); $promptaconsent = (bool)optional_param('promptaconsent', 0, PARAM_BOOL); $justauth = (bool)optional_param('justauth', 0, PARAM_BOOL); - if (!empty($state)) { + if (!empty($state) && $selectaccount === false) { $requestparams = [ 'state' => $state, 'code' => $code, @@ -155,7 +176,7 @@ public function handleredirect() { if ($justauth === true) { $stateparams['justauth'] = true; } - $this->initiateauthrequest($promptlogin, $stateparams, $extraparams); + $this->initiateauthrequest($promptlogin, $stateparams, $extraparams, $selectaccount); } } @@ -186,10 +207,12 @@ public function user_login($username, $password = null) { * @param bool $promptlogin Whether to prompt for login or use existing session. * @param array $stateparams Parameters to store as state. * @param array $extraparams Additional parameters to send with the OIDC request. + * @param bool $selectaccount Whether to prompt the user to select an account. */ - public function initiateauthrequest($promptlogin = false, array $stateparams = array(), array $extraparams = array()) { + public function initiateauthrequest($promptlogin = false, array $stateparams = array(), array $extraparams = array(), + bool $selectaccount = false) { $client = $this->get_oidcclient(); - $client->authrequest($promptlogin, $stateparams, $extraparams); + $client->authrequest($promptlogin, $stateparams, $extraparams, $selectaccount); } /** diff --git a/classes/oidcclient.php b/classes/oidcclient.php index 46a750d..01ebcc0 100644 --- a/classes/oidcclient.php +++ b/classes/oidcclient.php @@ -167,9 +167,13 @@ public function get_endpoint($endpoint) { * @param bool $promptlogin Whether to prompt for login or use existing session. * @param array $stateparams Parameters to store as state. * @param array $extraparams Additional parameters to send with the OIDC request. + * @param bool $selectaccount Whether to prompt the user to select an account. * @return array Array of request parameters. */ - protected function getauthrequestparams($promptlogin = false, array $stateparams = array(), array $extraparams = array()) { + protected function getauthrequestparams($promptlogin = false, array $stateparams = array(), array $extraparams = array(), + bool $selectaccount = false) { + global $SESSION; + $nonce = 'N'.uniqid(); $params = [ @@ -188,6 +192,14 @@ protected function getauthrequestparams($promptlogin = false, array $stateparams if ($promptlogin === true) { $params['prompt'] = 'login'; + } else if ($selectaccount === true) { + $params['prompt'] = 'select_account'; + } else { + $silentloginmode = get_config('auth_oidc', 'silentloginmode'); + if ($silentloginmode) { + $params['prompt'] = 'none'; + $SESSION->silent_login_mode = true; + } } $domainhint = get_config('auth_oidc', 'domainhint'); @@ -247,8 +259,10 @@ protected function getnewstate($nonce, array $stateparams = array()) { * @param bool $promptlogin Whether to prompt for login or use existing session. * @param array $stateparams Parameters to store as state. * @param array $extraparams Additional parameters to send with the OIDC request. + * @param bool $selectaccount Whether to prompt the user to select an account. */ - public function authrequest($promptlogin = false, array $stateparams = array(), array $extraparams = array()) { + public function authrequest($promptlogin = false, array $stateparams = array(), array $extraparams = array(), + bool $selectaccount = false) { if (empty($this->clientid)) { throw new moodle_exception('erroroidcclientnocreds', 'auth_oidc'); } @@ -257,7 +271,7 @@ public function authrequest($promptlogin = false, array $stateparams = array(), throw new moodle_exception('erroroidcclientnoauthendpoint', 'auth_oidc'); } - $params = $this->getauthrequestparams($promptlogin, $stateparams, $extraparams); + $params = $this->getauthrequestparams($promptlogin, $stateparams, $extraparams, $selectaccount); $redirecturl = new moodle_url($this->endpoints['auth'], $params); redirect($redirecturl); } diff --git a/lang/en/auth_oidc.php b/lang/en/auth_oidc.php index 0f0fedc..059c186 100644 --- a/lang/en/auth_oidc.php +++ b/lang/en/auth_oidc.php @@ -128,6 +128,22 @@ $string['cfg_loginflow_authcode_desc'] = 'Using this flow, the user clicks the name of the IdP (See "Provider Display Name" above) on the Moodle login page and is redirected to the provider to log in. Once successfully logged in, the user is redirected back to Moodle where the Moodle login takes place transparently. This is the most standardized, secure way for the user log in.'; $string['cfg_loginflow_rocreds'] = 'Resource Owner Password Credentials Grant (deprecated)'; $string['cfg_loginflow_rocreds_desc'] = 'This login flow is deprecated and will be removed from the plugin soon.
Using this flow, the user enters their username and password into the Moodle login form like they would with a manual login. This will authorize the user with the IdP, but will not create a session on the IdP\'s site. For example, if using Microsoft 365 with OpenID Connect, the user will be logged in to Moodle but not the Microsoft 365 web applications. Using the authorization request is recommended if you want users to be logged in to both Moodle and the IdP. Note that not all IdP support this flow. This option should only be used when other authorization grant types are not available.'; +$string['cfg_silentloginmode_key'] = 'Silent Login Mode'; +$string['cfg_silentloginmode_desc'] = 'If enabled, Moodle will try to use the active session of a user authenticated to the configured authorization endpoint to log the user in.
+To use this feature, the following configurations are required: + +In order to avoid Moodle trying to use personal accounts or accounts from other tenants to login, it is also recommended to use tenant specific endpoints, rather than generic ones using "common" or "organization" etc. paths.
+
+For Microsoft IdPs, the user experience is as follows: +'; $string['oidcresource'] = 'Resource'; $string['oidcresource_help'] = 'The OpenID Connect resource for which to send the request.
Note this is paramater is not supported in Microsoft identity platform (v2.0) IdP type.'; diff --git a/settings.php b/settings.php index d4e85cd..b44ba05 100644 --- a/settings.php +++ b/settings.php @@ -67,6 +67,12 @@ $settings->add(new admin_setting_configcheckbox('auth_oidc/forceredirect', get_string('cfg_forceredirect_key', 'auth_oidc'), get_string('cfg_forceredirect_desc', 'auth_oidc'), 0)); + // Silent login mode. + $forceloginconfigurl = new moodle_url('/admin/settings.php', ['section' => 'sitepolicies']); + $settings->add(new admin_setting_configcheckbox('auth_oidc/silentloginmode', + get_string('cfg_silentloginmode_key', 'auth_oidc'), + get_string('cfg_silentloginmode_desc', 'auth_oidc', $forceloginconfigurl->out(false)), 0)); + // Auto-append. $settings->add(new admin_setting_configtext('auth_oidc/autoappend', get_string('cfg_autoappend_key', 'auth_oidc'), get_string('cfg_autoappend_desc', 'auth_oidc'), '', PARAM_TEXT));