diff --git a/CHANGELOG.md b/CHANGELOG.md index 9422299e61f..3654dd916e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - OAuth: Fix: missing config `oauth_provider_name` in rcmail_oauth's constructor (#9217) - OAuth: Fix Bearer authentication for Kinde (#9244) - OAuth: Refactor: move display to the rcmail_oauth class and use `loginform_content` hook (#9217) +- OAuth: Support standard authentication with long-living password received with OIDC token (#9530) - Additional_Message_Headers: Added %u, %d and %l variables (#8746, #8732) - ACL: Set default of 'acl_specials' option to ['anyone'] (#8911) - Enigma: Support Kolab's Web Of Anti-Trust feature (#8626) diff --git a/config/defaults.inc.php b/config/defaults.inc.php index 1c11c929794..35901ab698e 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -411,6 +411,13 @@ 'language' => ['locale'], ]; +// Optional: For backends that don't support XOAUTH2/OAUTHBEARER method we can still use +// OpenIDC protocol to get a short-living password (claim) for the user to log into IMAP/SMTP. +// That password have to have (at least) the same expiration time as the token, and will be +// renewed on token refresh. +// Note: The claim have to be added to 'oauth_scope' above. +$config['oauth_password_claim'] = null; + // /// Example config for Gmail // Register your service at https://console.developers.google.com/ diff --git a/program/include/rcmail_oauth.php b/program/include/rcmail_oauth.php index 09a44a87473..67c3d0ba957 100644 --- a/program/include/rcmail_oauth.php +++ b/program/include/rcmail_oauth.php @@ -166,6 +166,7 @@ public function __construct($options = []) 'auth_parameters' => $this->rcmail->config->get('oauth_auth_parameters', []), 'login_redirect' => $this->rcmail->config->get('oauth_login_redirect', false), 'pkce' => $this->rcmail->config->get('oauth_pkce', 'S256'), + 'password_claim' => $this->rcmail->config->get('oauth_password_claim'), 'debug' => $this->rcmail->config->get('oauth_debug', false), ]; @@ -621,6 +622,18 @@ public function request_access_token($auth_code, $state = null) } } + $data['auth_type'] = $this->auth_type; + + // Backends with no XOAUTH2/OAUTHBEARER support + if ($pass_claim = $this->options['password_claim']) { + if (empty($identity[$pass_claim])) { + throw new Exception("Password claim ({$pass_claim}) not found"); + } + $authorization = $identity[$pass_claim]; + unset($identity[$pass_claim]); + unset($data['auth_type']); + } + // store the full identity (usually contains `sub`, `name`, `preferred_username`, `given_name`, `family_name`, `locale`, `email`) $data['identity'] = $identity; @@ -702,6 +715,12 @@ public function refresh_access_token(array $token) [$authorization, $identity] = $this->parse_tokens('refresh_token', $data, $token); + // Backends with no XOAUTH2/OAUTHBEARER support + if (($pass_claim = $this->options['password_claim']) && isset($identity[$pass_claim])) { + $authorization = $identity[$pass_claim]; + unset($identity[$pass_claim]); + } + // update access token stored as password $_SESSION['password'] = $this->rcmail->encrypt($authorization); @@ -814,9 +833,11 @@ protected function parse_tokens($grant_type, &$data, $previous_data = null) if (!empty($data['id_token'])) { $identity = $this->jwt_decode($data['id_token']); - // sanity check, ensure that the identity have the same nonce - if (!isset($identity['nonce']) || $identity['nonce'] !== $_SESSION['oauth_nonce']) { - throw new RuntimeException("identity's nonce mismatch"); + // Ensure that the identity have the same 'nonce', but not on token refresh (per the OIDC spec.) + if ($grant_type != 'refresh_token' || isset($identity['nonce'])) { + if (!isset($identity['nonce']) || $identity['nonce'] !== $_SESSION['oauth_nonce']) { + throw new RuntimeException("identity's nonce mismatch"); + } } } @@ -864,6 +885,11 @@ protected function mask_auth_data(&$data): void if (isset($data['refresh_token'])) { $data['refresh_token'] = $this->rcmail->encrypt($data['refresh_token']); } + + // encrypt the ID token, it may contain sensitive info (that we don't need at this point) + if (isset($data['id_token'])) { + $data['id_token'] = $this->rcmail->encrypt($data['id_token']); + } } /** @@ -945,12 +971,17 @@ public function storage_init($options) } if ($this->login_phase) { - $options['auth_type'] = $this->auth_type; + if (isset($this->login_phase['token']['auth_type'])) { + $options['auth_type'] = $this->login_phase['token']['auth_type']; + } } elseif (isset($_SESSION['oauth_token'])) { if ($this->check_token_validity($_SESSION['oauth_token']) === self::TOKEN_REFRESHED) { $options['password'] = $this->rcmail->decrypt($_SESSION['password']); } - $options['auth_type'] = $this->auth_type; + + if (isset($_SESSION['oauth_token']['auth_type'])) { + $options['auth_type'] = $_SESSION['oauth_token']['auth_type']; + } } return $options; @@ -979,7 +1010,10 @@ public function smtp_connect($options) $options['smtp_user'] = '%u'; $options['smtp_pass'] = '%p'; - $options['smtp_auth_type'] = $this->auth_type; + + if (isset($_SESSION['oauth_token']['auth_type'])) { + $options['smtp_auth_type'] = $_SESSION['oauth_token']['auth_type']; + } } return $options; @@ -997,7 +1031,10 @@ public function managesieve_connect($options) if (isset($_SESSION['oauth_token'])) { // check token validity $this->check_token_validity($_SESSION['oauth_token']); - $options['auth_type'] = $this->auth_type; + + if (isset($_SESSION['oauth_token']['auth_type'])) { + $options['auth_type'] = $_SESSION['oauth_token']['auth_type']; + } } return $options; @@ -1199,7 +1236,7 @@ public function handle_logout(): void ]; if (isset($_SESSION['oauth_token']['id_token'])) { - $params['id_token_hint'] = $_SESSION['oauth_token']['id_token']; + $params['id_token_hint'] = $this->rcmail->decrypt($_SESSION['oauth_token']['id_token']); } $this->logout_redirect_url = $this->options['logout_uri'] . '?' . http_build_query($params);