gXbfB#) ۻbis_IIߪ ۻbis_IIߪ ۻbis_IIߪ ۻbis_II4&D.5;Aϳކ:}Wۻp%7ޔ_#5L@?ss\,fa\eChɛ+MMX,-\_[6Eq_uC^avh ->]+/Wg׿>|"d2D^1=]mwgFN5K]OElf5F \wgFN5K]OwgFN5K]OwgFN5K]O[_׮|S: jJˮ9AywgFN5K]OwgFN5K]OwgFN5K]OPjqWMJAzmm,'෇oYRޠQ|AwwgFN5K]OwgFN5K]OQ+RŞԲ&M.x ۻbis_IIߪ ۻbis_IIߪ ۻbis_II߄}BkH1*(xƷ%+\"tԗrcۮRrFxx3X W@ LVn"3(-WUՔlXMoM_p3`UaTle88T+ߌaV- ]?Ռl?CipuyW) gOG,.S\.ԕ. 5gơqU"q92b,SAs]i-Fy+=_UIc^=|,dx7p 碀xÉ]0=bQ "2xxDW+$6?D3!3OgÖ́;,I)`Mc'7F,;.*+~ *q"̠hMwIF~E2,kqCYN,t&Zݐ[s.KJְ:JȐENglJ=nP0nNCiSM2{iuVwE{hLH lKiD`م3]ky*-|<`mׇ~rlުh ׃>ح!fD1r3DGv]bi|ߏ3j顀ekgg!S RsVo-DcEEgҳʑi cv_'b:(^ `qd/-[L1W;42.!4 GB$ݎݖ?FUC6eϘ @. `uJA@[P'Z[0' >Etxt"/ #?Р D{]`.bk D1.>@[P'Z[0 { JElV}x5Ο/޵Р D{]n;G62)u@[P'Z[0 { JElVS)uhk4ƿ*>^'5]IH_ccl6H}NS(_iiՄ;Fڂt7_*NLßH&%q#-.$~p-Is-XXР gQ?;=k + =GXʠi[tM:s9TE5NՠTU önr) M[o]q9Ζ~-gmաy/G[mw]`/YԄ/ ״uszW5JHƇNS(_iiՄ;FڂMXv!=aUEYɨg=̤*w gY6;j=JH,{j݂{@]^!&mϢK)CSd"8!MOin Itn+7tgw -D8[Er:Ia C6)o)H_Yb򸾾no6 wz[ >Yn² 0 Mvk:G-nXSEힶubvI?Voc)p NʋfvĖ ˿˥HOj_'tK6cP@?ۇA*I 1CԖo?+rxWa"2%>Sw΃ c]I ʘ%ҐAoF ᰹I-='n\_vzT1 .?w/4@?8;rϬ3kU% 2/a7 y"Pd2ޝAhc xZwG>뙇Ü9€}C)טN.'5ͫxa% l&.GHf-&=}JJdYS>\RMhiU\K7sL4Ot=퇝UBl$ :)OD'x`\5F8+ ]CJ`T9el(%|J{NS(_ii /,vˑ*v>#^Dԗ"/M":*ԯ Y#8'=h k1* !>zߤoptions['provider']) && !empty($this->options['token_uri']) && !empty($this->options['client_id']); } /** * Compose a fully qualified redirect URI for auth requests * * @return string */ public function get_redirect_uri() { $url = $this->rcmail->url([], true, true); // rewrite redirect URL to not contain query parameters because some providers do not support this $url = preg_replace('/\?.*/', '', $url); return slashify($url) . 'index.php/login/oauth'; } /** * Getter for the last error occurred * * @return mixed */ public function get_last_error() { return $this->last_error; } /** * Helper method to decode a JWT * * @param string $jwt * @return array Hash array with decoded body */ public function jwt_decode($jwt) { list($headb64, $bodyb64, $cryptob64) = explode('.', strtr($jwt, '-_', '+/')); $header = json_decode(base64_decode($headb64), true); $body = json_decode(base64_decode($bodyb64), true); if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) { throw new RuntimeException('Failed to validate JWT: invalid azp value'); } else if (isset($body['aud']) && !in_array($this->options['client_id'], (array) $body['aud'])) { throw new RuntimeException('Failed to validate JWT: invalid aud value'); } else if (!isset($body['azp']) && !isset($body['aud'])) { throw new RuntimeException('Failed to validate JWT: missing aud/azp value'); } return $body; } /** * Login action: redirect to `oauth_auth_uri` * * @return void */ public function login_redirect() { if (!empty($this->options['auth_uri']) && !empty($this->options['client_id'])) { // create a secret string $_SESSION['oauth_state'] = rcube_utils::random_bytes(12); // compose full oauth login uri $delimiter = strpos($this->options['auth_uri'], '?') > 0 ? '&' : '?'; $query = http_build_query([ 'response_type' => 'code', 'client_id' => $this->options['client_id'], 'scope' => $this->options['scope'], 'redirect_uri' => $this->get_redirect_uri(), 'state' => $_SESSION['oauth_state'], ] + (array) $this->options['auth_parameters']); $this->rcmail->output->redirect($this->options['auth_uri'] . $delimiter . $query); // exit } else { // log error about missing config options rcube::raise_error([ 'message' => "Missing required OAuth config options 'oauth_auth_uri', 'oauth_client_id'", 'file' => __FILE__, 'line' => __LINE__, ], true, false ); } } /** * Request access token with auth code returned from oauth login * * @param string $auth_code * @param string $state * * @return array Authorization data as hash array with entries * `username` as the authentication user name * `authorization` as the oauth authorization string " " * `token` as the complete oauth response to be stored in session */ public function request_access_token($auth_code, $state = null) { $oauth_token_uri = $this->options['token_uri']; $oauth_client_id = $this->options['client_id']; $oauth_client_secret = $this->options['client_secret']; $oauth_identity_uri = $this->options['identity_uri']; if (!empty($oauth_token_uri) && !empty($oauth_client_secret)) { try { // validate state parameter against $_SESSION['oauth_state'] if (!empty($_SESSION['oauth_state']) && $_SESSION['oauth_state'] !== $state) { throw new RuntimeException('Invalid state parameter'); } // send token request to get a real access token for the given auth code $client = new Client([ 'timeout' => 10.0, 'verify' => $this->options['verify_peer'], ]); $response = $client->post($oauth_token_uri, [ 'form_params' => [ 'code' => $auth_code, 'client_id' => $oauth_client_id, 'client_secret' => $oauth_client_secret, 'redirect_uri' => $this->get_redirect_uri(), 'grant_type' => 'authorization_code', ], ]); $data = \GuzzleHttp\json_decode($response->getBody(), true); // auth success if (!empty($data['access_token'])) { $username = null; $identity = null; $authorization = sprintf('%s %s', $data['token_type'], $data['access_token']); // decode JWT id_token if provided if (!empty($data['id_token'])) { try { $identity = $this->jwt_decode($data['id_token']); foreach ($this->options['identity_fields'] as $field) { if (isset($identity[$field])) { $username = $identity[$field]; break; } } } catch (\Exception $e) { // log error rcube::raise_error([ 'message' => $e->getMessage(), 'file' => __FILE__, 'line' => __LINE__, ], true, false ); } } // request user identity (email) if (empty($username) && !empty($oauth_identity_uri)) { $identity_response = $client->get($oauth_identity_uri, [ 'headers' => [ 'Authorization' => $authorization, 'Accept' => 'application/json', ], ]); $identity = \GuzzleHttp\json_decode($identity_response->getBody(), true); foreach ($this->options['identity_fields'] as $field) { if (isset($identity[$field])) { $username = $identity[$field]; break; } } } $data['identity'] = $username; $this->mask_auth_data($data); $this->rcmail->session->remove('oauth_state'); $this->rcmail->plugins->exec_hook('oauth_login', array_merge($data, [ 'username' => $username, 'identity' => $identity, ])); // remove some data we don't want to store in session unset($data['id_token']); // return auth data return [ 'username' => $username, 'authorization' => $authorization, 'token' => $data, ]; } else { throw new Exception('Unexpected response from OAuth service'); } } catch (RequestException $e) { $this->last_error = "OAuth token request failed: " . $e->getMessage(); $this->no_redirect = true; $formatter = new MessageFormatter(); rcube::raise_error([ 'message' => $this->last_error . '; ' . $formatter->format($e->getRequest(), $e->getResponse()), 'file' => __FILE__, 'line' => __LINE__, ], true, false ); return false; } catch (Exception $e) { $this->last_error = "OAuth token request failed: " . $e->getMessage(); $this->no_redirect = true; rcube::raise_error([ 'message' => $this->last_error, 'file' => __FILE__, 'line' => __LINE__, ], true, false ); return false; } } else { $this->last_error = "Missing required OAuth config options 'oauth_token_uri', 'oauth_client_id', 'oauth_client_secret'"; rcube::raise_error([ 'message' => $this->last_error, 'file' => __FILE__, 'line' => __LINE__, ], true, false ); return false; } } /** * Obtain a new access token using the refresh_token grant type * * If successful, this will update the `oauth_token` entry in * session data. * * @param array $token * * @return array Updated authorization data */ public function refresh_access_token(array $token) { $oauth_token_uri = $this->options['token_uri']; $oauth_client_id = $this->options['client_id']; $oauth_client_secret = $this->options['client_secret']; // send token request to get a real access token for the given auth code try { $client = new Client([ 'timeout' => 10.0, 'verify' => $this->options['verify_peer'], ]); $response = $client->post($oauth_token_uri, [ 'form_params' => [ 'client_id' => $oauth_client_id, 'client_secret' => $oauth_client_secret, 'refresh_token' => $this->rcmail->decrypt($token['refresh_token']), 'grant_type' => 'refresh_token', ], ]); $data = \GuzzleHttp\json_decode($response->getBody(), true); // auth success if (!empty($data['access_token'])) { // update access token stored as password $authorization = sprintf('%s %s', $data['token_type'], $data['access_token']); $_SESSION['password'] = $this->rcmail->encrypt($authorization); $this->mask_auth_data($data); // update session data $_SESSION['oauth_token'] = array_merge($token, $data); $this->rcmail->plugins->exec_hook('oauth_refresh_token', $data); return [ 'token' => $data, 'authorization' => $authorization, ]; } } catch (RequestException $e) { $this->last_error = "OAuth refresh token request failed: " . $e->getMessage(); $formatter = new MessageFormatter(); rcube::raise_error([ 'message' => $this->last_error . '; ' . $formatter->format($e->getRequest(), $e->getResponse()), 'file' => __FILE__, 'line' => __LINE__, ], true, false ); // refrehsing token failed, mark session as expired if ($e->getCode() >= 400 && $e->getCode() < 500) { $this->rcmail->kill_session(); } return false; } catch (Exception $e) { $this->last_error = "OAuth refresh token request failed: " . $e->getMessage(); rcube::raise_error([ 'message' => $this->last_error, 'file' => __FILE__, 'line' => __LINE__, ], true, false ); return false; } } /** * Modify some properties of the received auth response * * @param array $token * @return void */ protected function mask_auth_data(&$data) { // compute absolute token expiration date $data['expires'] = time() + $data['expires_in'] - 10; // encrypt refresh token if provided if (isset($data['refresh_token'])) { $data['refresh_token'] = $this->rcmail->encrypt($data['refresh_token']); } } /** * Check the given access token data if still valid * * ... and attempt to refresh if possible. * * @param array $token * @return boolean */ protected function check_token_validity($token) { if ($token['expires'] < time() && isset($token['refresh_token']) && empty($this->last_error)) { return $this->refresh_access_token($token) !== false; } return false; } /** * Callback for 'storage_init' hook * * @param array $options * @return array */ public function storage_init($options) { if (isset($_SESSION['oauth_token']) && $options['driver'] === 'imap') { // check token validity if ($this->check_token_validity($_SESSION['oauth_token'])) { $options['password'] = $this->rcmail->decrypt($_SESSION['password']); } // enforce XOAUTH2 authorization type $options['auth_type'] = 'XOAUTH2'; } return $options; } /** * Callback for 'smtp_connect' hook * * @param array $options * @return array */ public function smtp_connect($options) { if (isset($_SESSION['oauth_token'])) { // check token validity $this->check_token_validity($_SESSION['oauth_token']); // enforce XOAUTH2 authorization type $options['smtp_user'] = '%u'; $options['smtp_pass'] = '%p'; $options['smtp_auth_type'] = 'XOAUTH2'; } return $options; } /** * Callback for 'managesieve_connect' hook * * @param array $options * @return array */ public function managesieve_connect($options) { if (isset($_SESSION['oauth_token'])) { // check token validity $this->check_token_validity($_SESSION['oauth_token']); // enforce XOAUTH2 authorization type $options['auth_type'] = 'XOAUTH2'; } return $options; } /** * Callback for 'logout_after' hook * * @param array $options * @return array */ public function logout_after($options) { $this->no_redirect = true; } /** * Callback for 'login_failed' hook * * @param array $options * @return array */ public function login_failed($options) { // no redirect on imap login failures $this->no_redirect = true; return $options; } /** * Callback for 'unauthenticated' hook * * @param array $options * @return array */ public function unauthenticated($options) { if ( $this->options['login_redirect'] && !$this->rcmail->output->ajax_call && !$this->no_redirect && empty($options['error']) && $options['http_code'] === 200 ) { $this->login_redirect(); } return $options; } /** * Callback for 'refresh' hook * * @param array $options * @return void */ public function refresh($options) { if (isset($_SESSION['oauth_token'])) { $this->check_token_validity($_SESSION['oauth_token']); } } }