From 4a70021f41eceae5e841f15ea99de98683e9a423 Mon Sep 17 00:00:00 2001 From: ralf Date: Thu, 12 Jan 2023 19:33:31 -0600 Subject: [PATCH] WIP Oauth authentication for Office365: - add all Microsoft email domains - using login.microsoftonline.com/common as OAuth provider URL - use mail-server name to detect custom mail domains --> auth with IMAP agains outlook.office365.com still NOT working, probably needs some kind of further verification / being an Microsoft partner --- admin/inc/class.admin_mail.inc.php | 38 ++++++++++++----- api/src/Auth/OpenIDConnectClient.php | 63 ++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/admin/inc/class.admin_mail.inc.php b/admin/inc/class.admin_mail.inc.php index 4db3bd35b2..cfe433c710 100644 --- a/admin/inc/class.admin_mail.inc.php +++ b/admin/inc/class.admin_mail.inc.php @@ -33,7 +33,7 @@ class admin_mail /** * Enable logging of IMAP communication to given path, eg. /tmp/autoconfig.log */ - const DEBUG_LOG = null; + const DEBUG_LOG = '/var/lib/egroupware/imap.log'; /** * Connection timeout in seconds used in autoconfig, can and should be really short! */ @@ -233,14 +233,7 @@ class admin_mail { $content['output'] .= lang('Using IMAP:%1, SMTP:%2, OAUTH:%3:', $oauth['imap'], $oauth['smtp'], $oauth['provider'])."\n"; $hosts[$oauth['imap']] = true; - $content['acc_smpt_host'] = $oauth['smtp']; - $content['acc_sieve_enabled'] = false; - $content['acc_oauth_provider_url'] = $oauth['provider']; - $content['acc_oauth_client_id'] = $oauth['client']; - $content['acc_oauth_client_secret'] = $oauth['secret']; - $content['acc_oauth_scopes'] = $oauth['scopes']; - $content[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN] = $oauth[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN] ?? null; - $content[OpenIDConnectClient::ADD_AUTH_PARAM] = $oauth[OpenIDConnectClient::ADD_AUTH_PARAM] ?? null; + $content += self::oauth2content($oauth); } elseif (!empty($content['acc_imap_host'])) { @@ -292,6 +285,11 @@ class admin_mail // iterate over all hosts and try to connect foreach(!isset($connected) ? $hosts : [] as $host => $data) { + // check if we support OAuth for the (manual) configured mail-server + if (empty($content['acc_oauth_provider_url']) && ($oauth = OpenIDConnectClient::providerByDomain($content['acc_imap_username'], $host))) + { + $content += self::oauth2content($oauth); + } $content['acc_imap_host'] = $host; // by default we check SSL, STARTTLS and at last an insecure connection if (!is_array($data)) $data = array('TLS' => 993, 'SSL' => 993, 'STARTTLS' => 143, 'insecure' => 143); @@ -375,6 +373,26 @@ class admin_mail array_diff_key($content, ['output'=>true]), 2); } + /** + * Convert OAuth provider data to our content-names + * + * @param array $oauth + * @return array + */ + protected static function oauth2content(array $oauth) + { + return [ + 'acc_smpt_host' => $oauth['smtp'], + 'acc_sieve_enabled' => false, + 'acc_oauth_provider_url' => $oauth['provider'], + 'acc_oauth_client_id' => $oauth['client'], + 'acc_oauth_client_secret' => $oauth['secret'], + 'acc_oauth_scopes' => $oauth['scopes'], + OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN => $oauth[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN] ?? null, + OpenIDConnectClient::ADD_AUTH_PARAM => $oauth[OpenIDConnectClient::ADD_AUTH_PARAM] ?? null, + ]; + } + /** * Step 2: Folder - let user select trash, sent, drafs and template folder * @@ -1492,7 +1510,7 @@ class admin_mail // Google requires access_type=offline&prompt=consent to return a refresh-token if (!empty($content[OpenIDConnectClient::ADD_AUTH_PARAM])) { - $oidc->addAuthParam($content[OpenIDConnectClient::ADD_AUTH_PARAM]); + $oidc->addAuthParam(str_replace('$username', $content['acc_oauth_username'] ?? $content['acc_imap_username'], $content[OpenIDConnectClient::ADD_AUTH_PARAM])); } // we need to use response_code=query / GET request to keep our session token! diff --git a/api/src/Auth/OpenIDConnectClient.php b/api/src/Auth/OpenIDConnectClient.php index a775a381f7..ccd7d7ccc9 100644 --- a/api/src/Auth/OpenIDConnectClient.php +++ b/api/src/Auth/OpenIDConnectClient.php @@ -51,15 +51,22 @@ class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient * @var array[] email-regexp => [imap-host, smtp-host, oauth-provider, client-id, client-secret, scopes] pairs */ public static $oauth_domain_regexps = [ - '/(^|@)([^.@]+\.onmicrosoft\.com)$/i' => ['outlook.office365.com', 'smtp.office365.com', 'login.microsoftonline.com/$2', + // MS domains from https://www.internetearnings.com/how-to-register-live-or-hotmail-e-mail-address/ + '/(^|@)([^.@]+\.onmicrosoft\.com|'. + 'outlook\.(sa|com|com\.(ar|au|cz|gr|in|tw|tr|vn)|co\.(in|th)|at|cl|fr|de|hu|ie|it|jp|kr|lv|my|ph|pt|sg|sk|es)|'. + 'hotmail\.(com|com\.(ar|au|br|hk|tr|vn)|co\.(in|il|jp|kr|za|th|uk)|be|ca|cz|cl|dk|fi|fr|gr|de|hu|it|lv|lt|my|nl|no|ph|rs|sg|sk|es|se)|'. + 'live\.(com|com\.(ar|br|my|mx|ph|pt|sg)|co\.(il|kr|za|uk)|at|be|ca|cl|cn|dk|fi|fr|de|hk|ie|it|jp|nl|no|ru|se)|'. + 'windowslive\.com|livemail\.tw)$/i' => ['outlook.office365.com', 'smtp.office365.com', 'login.microsoftonline.com/common', 'e09fe57b-ffc5-496e-9ef8-3e6c7d628c09', 'Hd18Q~t-8_-ImvPFXlh8DSFjWKYyvpUTqURRJc7i', - 'https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access email', - [self::ADD_CLIENT_TO_WELL_KNOWN => 'appid']], + 'https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access email', + [/*self::ADD_CLIENT_TO_WELL_KNOWN => 'appid',*/ self::ADD_AUTH_PARAM => ['login_hint' => '$username', 'approval_prompt' => 'auto']], + null], '/(^|@)g(oogle)?mail\.com$/i' => ['imap.gmail.com', 'smtp.gmail.com', 'accounts.google.com', '581021931838-unqjf9tivr9brnmo34rbsoj179ojp79p.apps.googleusercontent.com', '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', 'https://mail.google.com/ https://www.googleapis.com/auth/userinfo.email', // https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token - [self::ADD_AUTH_PARAM => ['access_type' => 'offline', 'prompt' => 'consent']]], + [self::ADD_AUTH_PARAM => ['access_type' => 'offline', 'prompt' => 'consent']], + '/^(imap|smtp|mail)\.g(oogle)?mail\.com$/i'], ]; public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null) @@ -71,19 +78,27 @@ class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient // ToDo: set proxy, if configured in EGroupware //$this->setHttpProxy("http://my.proxy.com:80/"); + + // login.microsoftonline.com/common returns as issuer an URL with {tenantid} + if ($this->getProviderURL() === 'https://login.microsoftonline.com/common') + { + $this->setIssuerValidator(new MicrosoftIssuerValidator($this)); + } } /** * Find server config by email-domain incl. oauth data * * @param string $domain domain or email address + * @param string $mailserver option name of imap or smtp server to identify the provider * @return array|null for keys provider, client, secret, scopes, imap, smtp */ - public static function providerByDomain($domain) + public static function providerByDomain($domain, $mailserver=null) { - foreach(self::$oauth_domain_regexps as $regexp => [$imap, $smtp, $provider, $client, $secret, $scopes, $extra]) + foreach(self::$oauth_domain_regexps as $regexp => [$imap, $smtp, $provider, $client, $secret, $scopes, $extra, $server_regexp]) { - if (preg_match($regexp, $domain, $matches)) + if (preg_match($regexp, $domain, $matches) || + !empty($mailserver) && (in_array($mailserver, [$imap, $smtp]) || !empty($server_regexp) && preg_match($server_regexp, $mailserver))) { return [ 'imap' => $imap, @@ -124,7 +139,7 @@ class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient // Google requires access_type=offline to return a refresh-token if (!empty($provider[self::ADD_AUTH_PARAM])) { - $oidc->addAuthParam($provider[self::ADD_AUTH_PARAM]); + $oidc->addAuthParam(str_replace('$username', $domain, $provider[self::ADD_AUTH_PARAM])); } $oidc->addScope($provider['scopes']); @@ -208,4 +223,36 @@ class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient } call_user_func_array([$oidc, 'authenticateThen'], $authenticateThenParams); } +} + +/** + * login.microsoftonline.com/common returns as issuer an URL with {tenantid} + * + * We currently only check the there is some reasonable tenantid, not necessary the correct one, for which we would need to know the tenant. + */ +class MicrosoftIssuerValidator +{ + /** + * @var OpenIDConnectClient + */ + private $oidc; + + public function __construct(OpenIDConnectClient $oidc) + { + $this->oidc = $oidc; + } + + /** + * Validator for Microsoft issuer + * + * @param string $iss + * @return bool + * @throws OpenIDConnectClientException + */ + public function __invoke($iss) + { + $issuer_regexp = '#^'.str_replace('{tenantid}', '[a-f0-9-]+', $this->oidc->getWellKnownIssuer()).'$#'; + + return (bool)preg_match($issuer_regexp, $iss); + } } \ No newline at end of file