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
*/
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!

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
*/
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']);
@ -209,3 +224,35 @@ 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);
}
}