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
This commit is contained in:
ralf 2023-01-12 19:33:31 -06:00
parent 5d385455d2
commit 4a70021f41
2 changed files with 83 additions and 18 deletions

View File

@ -33,7 +33,7 @@ class admin_mail
/** /**
* Enable logging of IMAP communication to given path, eg. /tmp/autoconfig.log * 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! * 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"; $content['output'] .= lang('Using IMAP:%1, SMTP:%2, OAUTH:%3:', $oauth['imap'], $oauth['smtp'], $oauth['provider'])."\n";
$hosts[$oauth['imap']] = true; $hosts[$oauth['imap']] = true;
$content['acc_smpt_host'] = $oauth['smtp']; $content += self::oauth2content($oauth);
$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;
} }
elseif (!empty($content['acc_imap_host'])) elseif (!empty($content['acc_imap_host']))
{ {
@ -292,6 +285,11 @@ class admin_mail
// iterate over all hosts and try to connect // iterate over all hosts and try to connect
foreach(!isset($connected) ? $hosts : [] as $host => $data) 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; $content['acc_imap_host'] = $host;
// by default we check SSL, STARTTLS and at last an insecure connection // 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); 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); 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 * 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 // Google requires access_type=offline&prompt=consent to return a refresh-token
if (!empty($content[OpenIDConnectClient::ADD_AUTH_PARAM])) 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! // we need to use response_code=query / GET request to keep our session token!

View File

@ -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 * @var array[] email-regexp => [imap-host, smtp-host, oauth-provider, client-id, client-secret, scopes] pairs
*/ */
public static $oauth_domain_regexps = [ 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', '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', '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_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', '/(^|@)g(oogle)?mail\.com$/i' => ['imap.gmail.com', 'smtp.gmail.com', 'accounts.google.com',
'581021931838-unqjf9tivr9brnmo34rbsoj179ojp79p.apps.googleusercontent.com', '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', '581021931838-unqjf9tivr9brnmo34rbsoj179ojp79p.apps.googleusercontent.com', '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',
'https://mail.google.com/ https://www.googleapis.com/auth/userinfo.email', 'https://mail.google.com/ https://www.googleapis.com/auth/userinfo.email',
// https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token // 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) 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 // ToDo: set proxy, if configured in EGroupware
//$this->setHttpProxy("http://my.proxy.com:80/"); //$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 * Find server config by email-domain incl. oauth data
* *
* @param string $domain domain or email address * @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 * @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 [ return [
'imap' => $imap, 'imap' => $imap,
@ -124,7 +139,7 @@ class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient
// Google requires access_type=offline to return a refresh-token // Google requires access_type=offline to return a refresh-token
if (!empty($provider[self::ADD_AUTH_PARAM])) 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']); $oidc->addScope($provider['scopes']);
@ -209,3 +224,35 @@ class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient
call_user_func_array([$oidc, 'authenticateThen'], $authenticateThenParams); 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);
}
}