forked from extern/egroupware
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:
parent
5d385455d2
commit
4a70021f41
@ -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!
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user