mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-27 17:18:54 +01:00
WIP OAuth authentication for mail: working now with Gmail
This commit is contained in:
parent
f85aa4dcbb
commit
14b6a9a5ab
@ -153,23 +153,6 @@ class admin_mail
|
||||
*/
|
||||
public static $no_sieve_blacklist = array('gmail.com', 'googlemail.com', 'outlook.office365.com');
|
||||
|
||||
const ADD_CLIENT_TO_WELL_KNOWN = 'add-client-to-well-known';
|
||||
|
||||
/**
|
||||
* Regular expressions to match domain in username to imap/smtp servers and oauth provider
|
||||
*
|
||||
* @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/$1',
|
||||
'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',
|
||||
[self::ADD_CLIENT_TO_WELL_KNOWN => 'appid']],
|
||||
'/@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'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Is current use a mail administrator / has run rights for EMailAdmin
|
||||
*
|
||||
@ -223,31 +206,6 @@ class admin_mail
|
||||
), $readonlys, $content, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find server config by email-domain incl. oauth data
|
||||
*
|
||||
* @param $email
|
||||
* @return array|null
|
||||
*/
|
||||
public static function oauthDomainMatch($email)
|
||||
{
|
||||
foreach(self::$oauth_domain_regexps as $regexp => [$imap, $smtp, $provider, $client, $secret, $scopes, $extra])
|
||||
{
|
||||
if (preg_match($regexp, $email, $matches))
|
||||
{
|
||||
return [
|
||||
'imap' => $imap,
|
||||
'smtp' => $smtp,
|
||||
'provider' => $provider ? 'https://'.strtr($provider, ['$1' => $matches[1] ?? null, '$2' => $matches[2] ?? null]): null,
|
||||
'client' => $client,
|
||||
'secret' => $secret,
|
||||
'scopes' => array_merge(['openid'], explode(' ', $scopes)),
|
||||
]+($extra ?? []);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to autoconfig an account
|
||||
*
|
||||
@ -270,7 +228,21 @@ class admin_mail
|
||||
{
|
||||
$content['acc_imap_username'] = $content['ident_email'];
|
||||
}
|
||||
if (!empty($content['acc_imap_host']))
|
||||
// supported oauth providers
|
||||
if (($oauth = OpenIDConnectClient::providerByDomain($content['acc_imap_username'])))
|
||||
{
|
||||
$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;
|
||||
}
|
||||
elseif (!empty($content['acc_imap_host']))
|
||||
{
|
||||
$hosts = array($content['acc_imap_host'] => true);
|
||||
if ($content['acc_imap_port'] > 0 && !in_array($content['acc_imap_port'], array(143,993)))
|
||||
@ -282,19 +254,6 @@ class admin_mail
|
||||
);
|
||||
}
|
||||
}
|
||||
// supported oauth providers
|
||||
elseif (($oauth = self::oauthDomainMatch($content['acc_imap_username'])))
|
||||
{
|
||||
$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[self::ADD_CLIENT_TO_WELL_KNOWN] = $oauth[self::ADD_CLIENT_TO_WELL_KNOWN] ?? null;
|
||||
}
|
||||
elseif (($ispdb = self::mozilla_ispdb($content['ident_email'])) && count($ispdb['imap']))
|
||||
{
|
||||
$content['ispdb'] = $ispdb;
|
||||
@ -324,7 +283,7 @@ class admin_mail
|
||||
}
|
||||
|
||||
// check if support OAuth for that domain or we have a password
|
||||
if (empty($oauth) && empty($content['acc_imap_password']))
|
||||
if (empty($oauth) && empty($content['acc_oauth_provider_url']) && empty($content['acc_imap_password']))
|
||||
{
|
||||
Etemplate::set_validation_error('acc_imap_password', lang('Field must not be empty!'));
|
||||
$connected = false;
|
||||
@ -773,7 +732,7 @@ class admin_mail
|
||||
$content['smtp_output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl connection to $host:$port ...\n";
|
||||
$content['acc_smtp_port'] = $port;
|
||||
|
||||
$mail = new Horde_Mail_Transport_Smtphorde($params=array(
|
||||
$params = [
|
||||
'username' => $content['acc_smtp_username'],
|
||||
'password' => $content['acc_smtp_password'],
|
||||
'host' => $content['acc_smtp_host'],
|
||||
@ -781,7 +740,12 @@ class admin_mail
|
||||
'secure' => self::$ssl2secure[(string)array_search($content['acc_smtp_ssl'], self::$ssl2type)],
|
||||
'timeout' => self::TIMEOUT,
|
||||
'debug' => self::DEBUG_LOG,
|
||||
));
|
||||
];
|
||||
if (!empty($content['acc_oauth_provider_url']))
|
||||
{
|
||||
$params['xoauth2_token'] = self::oauthToken($content, true);
|
||||
}
|
||||
$mail = new Horde_Mail_Transport_Smtphorde($params);
|
||||
// create smtp connection and authenticate, if credentials given
|
||||
$smtp = $mail->getSMTPObject();
|
||||
$content['smtp_output'] .= "\n".lang('Successful connected to %1 server%2.', 'SMTP',
|
||||
@ -1503,7 +1467,7 @@ class admin_mail
|
||||
if (!empty($content['acc_oauth_provider_url']))
|
||||
{
|
||||
$config['xoauth2_token'] = self::oauthToken($content);
|
||||
if (empty($config['password'])) $config['password'] = 'xxx'; // some password is required, even if not used
|
||||
if (empty($config['password'])) $config['password'] = '**oauth**'; // some password is required, even if not used
|
||||
}
|
||||
return new Horde_Imap_Client_Socket($config);
|
||||
}
|
||||
@ -1511,7 +1475,7 @@ class admin_mail
|
||||
/**
|
||||
* Acquire OAuth access (and refresh) token
|
||||
*/
|
||||
protected static function oauthToken(array &$content)
|
||||
protected static function oauthToken(array &$content, bool $smtp=false)
|
||||
{
|
||||
if (empty($content['acc_oauth_client_secret']))
|
||||
{
|
||||
@ -1521,9 +1485,14 @@ class admin_mail
|
||||
$content['acc_oauth_client_id'], $content['acc_oauth_client_secret']);
|
||||
|
||||
// Office365 requires client-ID as appid GET parameter (https://github.com/jumbojett/OpenID-Connect-PHP/issues/190)
|
||||
if (!empty($content[self::ADD_CLIENT_TO_WELL_KNOWN]))
|
||||
if (!empty($content[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN]))
|
||||
{
|
||||
$oidc->setWellKnownConfigParameters([$content[self::ADD_CLIENT_TO_WELL_KNOWN] => $content['acc_oauth_client_id']]);
|
||||
$oidc->setWellKnownConfigParameters([$content[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN] => $content['acc_oauth_client_id']]);
|
||||
}
|
||||
// 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]);
|
||||
}
|
||||
|
||||
// we need to use response_code=query / GET request to keep our session token!
|
||||
@ -1537,9 +1506,13 @@ class admin_mail
|
||||
{
|
||||
$content['acc_oauth_access_token'] = $oidc->refreshToken($content['acc_oauth_refresh_token']);
|
||||
}
|
||||
return new Horde_Imap_Client_Password_Xoauth2($content['acc_imap_username'], $content['acc_oauth_access_token']);
|
||||
if ($smtp)
|
||||
{
|
||||
return new Horde_Smtp_Password_Xoauth2($content['acc_oauth_username'] ?? $content['acc_smtp_username'], $content['acc_oauth_access_token']);
|
||||
}
|
||||
return new Horde_Imap_Client_Password_Xoauth2($content['acc_oauth_username'] ?? $content['acc_imap_username'], $content['acc_oauth_access_token']);
|
||||
}
|
||||
// Run OAuth authentification, will NOT return, but call success or failure callbacks below
|
||||
// Run OAuth authentication, will NOT return, but call success or failure callbacks below
|
||||
$oidc->authenticateThen(__CLASS__.'::oauthAuthenticated', [$content], __CLASS__.'::oauthFailure', [$content]);
|
||||
}
|
||||
|
||||
@ -1552,8 +1525,16 @@ class admin_mail
|
||||
*/
|
||||
public static function oauthAuthenticated(OpenIDConnectClient $oidc, array $content)
|
||||
{
|
||||
if (empty($content['acc_oauth_username']))
|
||||
{
|
||||
$content['acc_oauth_username'] = $content['acc_imap_username'];
|
||||
}
|
||||
if (empty($content['acc_oauth_refresh_token'] = $oidc->getRefreshToken()))
|
||||
{
|
||||
$content['output'] .= lang('OAuth Authentiction').': '.lang('Successfull, but NO refresh-token received!');
|
||||
$content['connected'] = false;
|
||||
}
|
||||
$content['acc_oauth_access_token'] = $oidc->getAccessToken();
|
||||
$content['acc_oauth_refresh_token'] = $oidc->getRefreshToken();
|
||||
|
||||
$GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
|
||||
|
||||
@ -1570,6 +1551,7 @@ class admin_mail
|
||||
public static function oauthFailure(Throwable $exception=null, array $content)
|
||||
{
|
||||
$content['output'] .= lang('OAuth Authentiction').': '.($exception ? $exception->getMessage() : lang('failed'));
|
||||
$content['connected'] = false;
|
||||
|
||||
$GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
|
||||
|
||||
|
@ -585,6 +585,7 @@ ldap server admin password admin de LDAP-Server Administrator-Passwort
|
||||
ldap server hostname or ip address admin de LDAP-Server Hostname oder IP-Adresse
|
||||
ldap settings admin de LDAP-Einstellungen
|
||||
leave empty for no quota admin de leer lassen um Quota zu deaktivieren
|
||||
leave empty to use oauth, if supported admin de Leer lassen um OAuth zu benutzen, wenn unterstützt
|
||||
leave the category untouched and return back to the list admin de Kategorie unverändert lassen und zur Liste zurückkehren
|
||||
leave the group untouched and return back to the list admin de Gruppe unverändert lassen und zur Liste zurückkehren
|
||||
leave unchanged admin de unverändert lassen
|
||||
@ -653,6 +654,7 @@ no login history exists for this user admin de Benutzer hat sich noch nie angeme
|
||||
no matches found admin de Keine Übereinstimmungen gefunden
|
||||
no message returned. admin de Keine Nachricht zurückgeliefert.
|
||||
no modes available admin de Kein Modus verfügbar
|
||||
no oauth client secret for provider '%1'! admin de Kein OAuth Client Secret für den Provider '%1'!
|
||||
no permission to add groups admin de Sie haben keine ausreichenden Rechte eine Gruppe hinzuzufügen
|
||||
no permission to add users admin de Sie haben keine ausreichenden Rechte eine Benutzer hinzuzufügen
|
||||
no permission to create groups admin de Sie haben keine ausreichenden Rechte um eine Gruppe zu erstellen
|
||||
@ -673,6 +675,7 @@ number of sessions / egroupware logins in the last 30 days admin de Anzahl Sitzu
|
||||
number of users admin de Anzahl Benutzer
|
||||
number the applications serially. if they are not numbered serially, sorting the applications could work wrong. this will not change the application's order. admin de Numeriere Anwendungen aufeinander folgend. Wenn sie nicht aufeinander folgend numeriert sind, funktioniert das Sortieren der Anwendungen nicht. Ändert nicht die Reihenfolge der Anwendungen
|
||||
nummeric account id admin de numerische Benutzer ID
|
||||
oauth authentiction admin de OAuth Authentifizierung
|
||||
offer to installing egroupware as mail-handler admin de Anbieten EGroupware als Mail-Handler zu installieren
|
||||
official egroupware usage statistic admin de Offizielle EGroupware Nutzungsstatistik
|
||||
one day admin de ein Tag
|
||||
@ -861,6 +864,7 @@ subtype admin de Untertyp
|
||||
subversion checkout admin de Subversion checkout (svn)
|
||||
success admin de erfolgreich
|
||||
successful connected to %1 server%2. admin de Erfolgreich zu %1 Server verbunden%2.
|
||||
successfull, but no refresh-token received! admin de Erfolgreiche, aber keine Refresh-Token erhalten!
|
||||
switch back to standard identity to save account. admin de Kehren Sie zur Standard-Identität zurück um das Konto zu speichern.
|
||||
switch back to standard identity to save other account data. admin de Kehren Sie zur Standard-Identität zurück um andere Kontendaten zu speichern.
|
||||
switch it off, if users are randomly thrown out admin de schalten Sie es aus, wenn Benutzer immer wieder zufällig rausgeworfen werden
|
||||
@ -939,6 +943,7 @@ upload your logo or enter the url admin de Laden Sie Ihr Logo hoch oder geben Si
|
||||
uppercase, lowercase, number, special char admin de Großbuchstaben, Kleinbuchstaben, Zahlen, Sonderzeichen
|
||||
url of the egroupware installation, eg. http://domain.com/egroupware admin de URL der EGroupware Installation, z.B. http://domain.com/egroupware
|
||||
usage admin de Einsatz
|
||||
use admin credentials to connect without a session-password, e.g. for sso admin de Benutze Admin Zugangsdaten zur Verbindung ohne Sitzungspasswort, z.B. für SingleSignOn
|
||||
use cookies to pass sessionid admin de Sitzungs-ID in einem Cookie speichern
|
||||
use default admin de Vorgabe verwenden
|
||||
use ldap defaults admin de LDAP Standardeinstellungen benutzen
|
||||
@ -971,6 +976,7 @@ users can define their own signatures admin de Anwender können ihre eigenen Sig
|
||||
users can utilize these stationery templates admin de Benutzer können diese Briefpapiervorlagen verwenden
|
||||
users choice admin de Benutzerauswahl
|
||||
using data from mozilla ispdb for provider %1 admin de Benutzer Mozilla ISPDB für Provider %1
|
||||
using imap:%1, smtp:%2, oauth:%3: admin de Benutzer IMAP: %1, SMTP: %2, OAuth: %3:
|
||||
vacation messages with start- and end-date require an admin account to be set admin de Abwesenheitsnotizen mit Start- und Enddatum benötigen einen gesetzten Administrator Benutzer!
|
||||
vacation notice admin de Abwesenheitsnotiz
|
||||
value for column %1 is not unique! admin de Wert für die Spalte %1 ist nicht eindeutig!
|
||||
|
@ -588,6 +588,7 @@ ldap server admin password admin en LDAP server admin password
|
||||
ldap server hostname or ip address admin en LDAP server host name or IP address
|
||||
ldap settings admin en LDAP settings
|
||||
leave empty for no quota admin en Leave empty for no quota
|
||||
leave empty to use oauth, if supported admin en Leave empty to use OAuth, if supported
|
||||
leave the category untouched and return back to the list admin en Leave the category untouched and return back to the list.
|
||||
leave the group untouched and return back to the list admin en Leave the group untouched and return back to the list.
|
||||
leave unchanged admin en Leave unchanged
|
||||
@ -656,6 +657,7 @@ no login history exists for this user admin en No login history exists for this
|
||||
no matches found admin en No matches found!
|
||||
no message returned. admin en No message returned
|
||||
no modes available admin en No modes available!
|
||||
no oauth client secret for provider '%1'! admin en No OAuth client secret for provider '%1'!
|
||||
no permission to add groups admin en No permission to add groups!
|
||||
no permission to add users admin en No permission to add users!
|
||||
no permission to create groups admin en No permission to create groups!
|
||||
@ -676,6 +678,7 @@ number of sessions / egroupware logins in the last 30 days admin en Number of se
|
||||
number of users admin en Number of users
|
||||
number the applications serially. if they are not numbered serially, sorting the applications could work wrong. this will not change the application's order. admin en Number the applications serially. If they are not numbered serially, sorting the applications could work wrong. This will not change the application's order.
|
||||
nummeric account id admin en Numeric account ID
|
||||
oauth authentiction admin en OAuth Authentiction
|
||||
offer to installing egroupware as mail-handler admin en Offer to installing EGroupware as mail-handler
|
||||
official egroupware usage statistic admin en Official EGroupware usage statistic
|
||||
one day admin en One day
|
||||
@ -864,6 +867,7 @@ subtype admin en Sub type
|
||||
subversion checkout admin en Subversion checkout
|
||||
success admin en Success
|
||||
successful connected to %1 server%2. admin en Successful connected to %1 server%2.
|
||||
successfull, but no refresh-token received! admin en Successfull, but NO refresh-token received!
|
||||
switch back to standard identity to save account. admin en Switch back to standard identity to save account.
|
||||
switch back to standard identity to save other account data. admin en Switch back to standard identity to save other account data.
|
||||
switch it off, if users are randomly thrown out admin en Switch it off, if users are randomly logged out
|
||||
@ -942,6 +946,7 @@ upload your logo or enter the url admin en Upload your logo or enter the URL
|
||||
uppercase, lowercase, number, special char admin en Uppercase, lowercase, number, special char
|
||||
url of the egroupware installation, eg. http://domain.com/egroupware admin en URL of the EGroupware installation, e.g. http://domain.com/egroupware
|
||||
usage admin en Usage
|
||||
use admin credentials to connect without a session-password, e.g. for sso admin en Use admin credentials to connect without a session-password, e.g. for SSO
|
||||
use cookies to pass sessionid admin en Use cookies to pass session ID
|
||||
use default admin en use default
|
||||
use ldap defaults admin en Use LDAP defaults
|
||||
@ -974,6 +979,7 @@ users can define their own signatures admin en Users can define their own signat
|
||||
users can utilize these stationery templates admin en Users can utilize these stationery templates
|
||||
users choice admin en Users choice
|
||||
using data from mozilla ispdb for provider %1 admin en Using data from Mozilla ISPDB for provider %1
|
||||
using imap:%1, smtp:%2, oauth:%3: admin en Using IMAP:%1, SMTP:%2, OAUTH:%3:
|
||||
vacation messages with start- and end-date require an admin account to be set admin en Vacation messages with start and end date require an admin account to be set!
|
||||
vacation notice admin en Vacation notice
|
||||
value for column %1 is not unique! admin en Value for column %1 is not unique!
|
||||
|
@ -67,7 +67,7 @@
|
||||
</row>
|
||||
<row>
|
||||
<description for="acc_imap_password" value="Password"/>
|
||||
<passwd id="acc_imap_password" size="32" maxlength="128" autocomplete="off"/>
|
||||
<passwd id="acc_imap_password" size="32" maxlength="128" autocomplete="off" blur="Leave empty to use OAuth, if supported"/>
|
||||
<description id="acc_imap_account_id" class="emailadmin_diagnostic"/>
|
||||
<description/>
|
||||
</row>
|
||||
@ -220,7 +220,7 @@
|
||||
<row class="@manual_class">
|
||||
<description for="acc_smtp_password" value="Password"/>
|
||||
<hbox>
|
||||
<passwd id="acc_smtp_password" size="32" maxlength="128" autocomplete="off"/>
|
||||
<passwd id="acc_smtp_password" size="32" maxlength="128" autocomplete="off" blur="Leave empty to use OAuth, if supported"/>
|
||||
<description id="acc_smtp_account_id" class="emailadmin_diagnostic"/>
|
||||
</hbox>
|
||||
</row>
|
||||
|
@ -23,7 +23,7 @@
|
||||
</row>
|
||||
<row>
|
||||
<description value="Password" for="acc_imap_password"/>
|
||||
<passwd id="acc_imap_password" size="32" maxlength="128" autocomplete="off" blur="Can be left empty, if OAuth is supported" class="et2_required"/>
|
||||
<passwd id="acc_imap_password" size="32" maxlength="128" autocomplete="off" blur="Leave empty to use OAuth, if supported" class="et2_required"/>
|
||||
</row>
|
||||
<row class="@manual_class">
|
||||
<description value="IMAP server" for="acc_imap_host"/>
|
||||
|
@ -26,7 +26,7 @@ if (!empty($GLOBALS['egw_info']['server']['cookie_samesite_attribute']) && $GLOB
|
||||
* It also uses https://proxy.egroupware.org/oauth as redirect-url to be registered with providers, implemented by the following Nginx location block:
|
||||
*
|
||||
* location /oauth {
|
||||
* if ($arg_state ~ ^(?<redirect_host>[^&:%]+)(:|%3a)(?<redirect_path>[^&:%]+)(:|%3a)) {
|
||||
* if ($arg_state ~* ^(?<redirect_host>[^&:%]+)(:|%3a)(?<redirect_path>[^&:%]+)(:|%3a)) {
|
||||
* return 302 https://$redirect_host/$redirect_path/api/oauth.php?$args;
|
||||
* }
|
||||
* return 301 https://github.com/EGroupware/egroupware/blob/master/api/src/Auth/OpenIDConnectClient.php;
|
||||
@ -36,17 +36,100 @@ if (!empty($GLOBALS['egw_info']['server']['cookie_samesite_attribute']) && $GLOB
|
||||
* https://proxy.egroupware.org/oauth?state=test.egroupware.org:test:<state>&<other-args> --> https://test.egroupware.org/egroupware/api/oauth.php?<all-arguments>
|
||||
*
|
||||
* @link https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider
|
||||
* @link https://github.com/mozilla/releases-comm-central/blob/master/mailnews/base/src/OAuth2Providers.jsm
|
||||
*/
|
||||
class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient
|
||||
{
|
||||
const EGROUPWARE_OAUTH_PROXY = 'https://proxy.egroupware.org/oauth';
|
||||
|
||||
const ADD_CLIENT_TO_WELL_KNOWN = 'add-client-to-well-known';
|
||||
const ADD_AUTH_PARAM = 'add-auth-param';
|
||||
|
||||
/**
|
||||
* Regular expressions to match domain in username to imap/smtp servers and oauth provider
|
||||
*
|
||||
* @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',
|
||||
'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']],
|
||||
'/(^|@)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']]],
|
||||
];
|
||||
|
||||
public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null)
|
||||
{
|
||||
parent::__construct($provider_url, $client_id, $client_secret, $issuer);
|
||||
|
||||
// set https://proxy.egroupware.org/oauth as redirect URL, which redirects to host and path given in nonce parameter plus /api/oauth.php
|
||||
$this->setRedirectURL(self::EGROUPWARE_OAUTH_PROXY);
|
||||
|
||||
// ToDo: set proxy, if configured in EGroupware
|
||||
//$this->setHttpProxy("http://my.proxy.com:80/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find server config by email-domain incl. oauth data
|
||||
*
|
||||
* @param string $domain domain or email address
|
||||
* @return array|null for keys provider, client, secret, scopes, imap, smtp
|
||||
*/
|
||||
public static function providerByDomain($domain)
|
||||
{
|
||||
foreach(self::$oauth_domain_regexps as $regexp => [$imap, $smtp, $provider, $client, $secret, $scopes, $extra])
|
||||
{
|
||||
if (preg_match($regexp, $domain, $matches))
|
||||
{
|
||||
return [
|
||||
'imap' => $imap,
|
||||
'smtp' => $smtp,
|
||||
'provider' => $provider ? 'https://'.strtr($provider, ['$1' => $matches[1] ?? null, '$2' => $matches[2] ?? null]): null,
|
||||
'client' => $client,
|
||||
'secret' => $secret,
|
||||
'scopes' => array_merge(['openid'], explode(' ', $scopes)),
|
||||
]+($extra ?? []);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OIDC client object for the given domain/email
|
||||
*
|
||||
* @param string $domain domain or email address
|
||||
* @return self|null
|
||||
*/
|
||||
public static function byDomain($domain)
|
||||
{
|
||||
if (!($provider = self::providerByDomain($domain)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
$oidc = new self($provider['provider'], $provider['client'], $provider['secret']);
|
||||
|
||||
// we need to use response_code=query / GET request to keep our session token!
|
||||
$oidc->setResponseTypes(['code']); // to be able to use query, not 'id_token'
|
||||
|
||||
// Office365 requires client-ID as appid GET parameter (https://github.com/jumbojett/OpenID-Connect-PHP/issues/190)
|
||||
if (!empty($provider[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN]))
|
||||
{
|
||||
$oidc->setWellKnownConfigParameters([$provider[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN] => $provider['client']]);
|
||||
}
|
||||
|
||||
// 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->addScope($provider['scopes']);
|
||||
|
||||
return $oidc;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -555,7 +555,7 @@ class Account implements \ArrayAccess
|
||||
// Horde use locale for translation of error messages
|
||||
Api\Preferences::setlocale(LC_MESSAGES);
|
||||
|
||||
$this->smtpTransport = new Horde_Mail_Transport_Smtphorde(array(
|
||||
$config = [
|
||||
'username' => $params['acc_smtp_username'] ?? null,
|
||||
'password' => $params['acc_smtp_password'] ?? null,
|
||||
'host' => $params['acc_smtp_host'],
|
||||
@ -563,7 +563,13 @@ class Account implements \ArrayAccess
|
||||
'secure' => $secure,
|
||||
'debug' => self::SMTP_DEBUG_LOG,
|
||||
//'timeout' => self::TIMEOUT,
|
||||
));
|
||||
];
|
||||
// if we have an OAuth access-token for the user, pass it on
|
||||
if (!empty($params['acc_oauth_access_token']) && $config['username'] === $params['acc_oauth_username'])
|
||||
{
|
||||
$config['xoauth2_token'] = new \Horde_Smtp_Password_Xoauth2($params['acc_oauth_username'], $params['acc_oauth_access_token']);
|
||||
}
|
||||
$this->smtpTransport = new Horde_Mail_Transport_Smtphorde($config);
|
||||
}
|
||||
return $this->smtpTransport;
|
||||
}
|
||||
@ -1305,29 +1311,38 @@ class Account implements \ArrayAccess
|
||||
}
|
||||
// check for whom we have to store credentials
|
||||
$valid_for = self::credentials_valid_for($data, $user);
|
||||
// add imap credentials
|
||||
$cred_type = $data['acc_imap_username'] == $data['acc_smtp_username'] &&
|
||||
// add oauth credentials
|
||||
if (!empty($data['acc_oauth_username'] ?? $data['acc_imap_username']) && !empty($data['acc_oauth_refresh_token']))
|
||||
{
|
||||
Credentials::write($data['acc_id'], $data['acc_oauth_username'] ?? $data['acc_imap_username'], $data['acc_oauth_refresh_token'],
|
||||
$cred_type=Credentials::OAUTH_REFRESH_TOKEN, $valid_for, $data['acc_oauth_cred_id']);
|
||||
}
|
||||
else
|
||||
{
|
||||
// add imap credentials
|
||||
$cred_type = $data['acc_imap_username'] == $data['acc_smtp_username'] &&
|
||||
$data['acc_imap_password'] == $data['acc_smtp_password'] ? 3 : 1;
|
||||
// if both passwords are unavailable, they seem identical, do NOT store them together, as they are not!
|
||||
if ($cred_type == 3 && $data['acc_imap_password'] == Credentials::UNAVAILABLE &&
|
||||
$data['acc_imap_password'] == Credentials::UNAVAILABLE &&
|
||||
$data['acc_imap_cred_id'] != $data['acc_smtp_cred_id'])
|
||||
{
|
||||
$cred_type = 1;
|
||||
}
|
||||
Credentials::write($data['acc_id'], $data['acc_imap_username'], $data['acc_imap_password'],
|
||||
$cred_type, $valid_for, $data['acc_imap_cred_id']);
|
||||
// add smtp credentials if necessary and different from imap
|
||||
if ($data['acc_smtp_username'] && $cred_type != 3)
|
||||
{
|
||||
Credentials::write($data['acc_id'], $data['acc_smtp_username'], $data['acc_smtp_password'],
|
||||
2, $valid_for, $data['acc_smtp_cred_id'] != $data['acc_imap_cred_id'] ?
|
||||
$data['acc_smtp_cred_id'] : null);
|
||||
}
|
||||
// delete evtl. existing SMTP credentials, after storing IMAP&SMTP together now
|
||||
elseif ($data['acc_smtp_cred_id'])
|
||||
{
|
||||
Credentials::delete($data['acc_id'], $valid_for, Credentials::SMTP, true);
|
||||
// if both passwords are unavailable, they seem identical, do NOT store them together, as they are not!
|
||||
if ($cred_type == 3 && $data['acc_imap_password'] == Credentials::UNAVAILABLE &&
|
||||
$data['acc_imap_password'] == Credentials::UNAVAILABLE &&
|
||||
$data['acc_imap_cred_id'] != $data['acc_smtp_cred_id'])
|
||||
{
|
||||
$cred_type = 1;
|
||||
}
|
||||
Credentials::write($data['acc_id'], $data['acc_imap_username'], $data['acc_imap_password'],
|
||||
$cred_type, $valid_for, $data['acc_imap_cred_id']);
|
||||
// add smtp credentials if necessary and different from imap
|
||||
if ($data['acc_smtp_username'] && $cred_type != 3)
|
||||
{
|
||||
Credentials::write($data['acc_id'], $data['acc_smtp_username'], $data['acc_smtp_password'],
|
||||
2, $valid_for, $data['acc_smtp_cred_id'] != $data['acc_imap_cred_id'] ?
|
||||
$data['acc_smtp_cred_id'] : null);
|
||||
}
|
||||
// delete evtl. existing SMTP credentials, after storing IMAP&SMTP together now
|
||||
elseif ($data['acc_smtp_cred_id'])
|
||||
{
|
||||
Credentials::delete($data['acc_id'], $valid_for, Credentials::SMTP, true);
|
||||
}
|
||||
}
|
||||
|
||||
// store or delete admin credentials
|
||||
|
@ -15,6 +15,7 @@
|
||||
namespace EGroupware\Api\Mail;
|
||||
|
||||
use EGroupware\Api;
|
||||
use Jumbojett\OpenIDConnectClientException;
|
||||
|
||||
/**
|
||||
* Mail account credentials are stored in egw_ea_credentials for given
|
||||
@ -79,10 +80,15 @@ class Credentials
|
||||
*/
|
||||
const SPAMTITAN = 128;
|
||||
|
||||
/**
|
||||
* Refresh token for IMAP & SMTP via OAuth
|
||||
*/
|
||||
const OAUTH_REFRESH_TOKEN = 256;
|
||||
|
||||
/**
|
||||
* All credentials
|
||||
*/
|
||||
const ALL = self::IMAP|self::SMTP|self::ADMIN|self::SMIME|self::TWOFA|self::SPAMTITAN;
|
||||
const ALL = self::IMAP|self::SMTP|self::ADMIN|self::SMIME|self::TWOFA|self::SPAMTITAN|self::OAUTH_REFRESH_TOKEN;
|
||||
|
||||
/**
|
||||
* Password in cleartext
|
||||
@ -132,6 +138,7 @@ class Credentials
|
||||
self::SMIME => 'acc_smime_',
|
||||
self::TWOFA => '2fa_',
|
||||
self::SPAMTITAN => 'acc_spam_',
|
||||
self::OAUTH_REFRESH_TOKEN => 'acc_oauth_'
|
||||
);
|
||||
|
||||
/**
|
||||
@ -183,8 +190,8 @@ class Credentials
|
||||
'account_id' => $account_id,
|
||||
'(cred_type & '.(int)$type.') > 0', // postgreSQL require > 0, or gives error as it expects boolean
|
||||
), __LINE__, __FILE__, false,
|
||||
// account_id DESC ensures 0=all allways overwrite (old user-specific credentials)
|
||||
'ORDER BY account_id ASC', self::APP);
|
||||
// account_id DESC ensures 0=all always overwrite (old user-specific credentials)
|
||||
'ORDER BY account_id ASC, cred_type ASC', self::APP);
|
||||
//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") nothing in cache");
|
||||
}
|
||||
else
|
||||
@ -230,12 +237,56 @@ class Credentials
|
||||
$results[$prefix.'cred_id'] = $row['cred_id'];
|
||||
$results[$prefix.'account_id'] = $row['account_id'];
|
||||
$results[$prefix.'pw_enc'] = $row['cred_pw_enc'];
|
||||
|
||||
// for OAuth we return the access- and not the refresh-token
|
||||
if ($pattern == self::OAUTH_REFRESH_TOKEN)
|
||||
{
|
||||
unset($results[$prefix.'password']);
|
||||
$results[$prefix.'refresh_token'] = self::UNAVAILABLE; // no need to make it available
|
||||
$results[$prefix.'access_token'] = self::getAccessToken($row['cred_username'], $password);
|
||||
// if no extra imap&smtp username set, set the oauth one
|
||||
foreach(['acc_imap_', 'acc_smtp_'] as $pre)
|
||||
{
|
||||
if (empty($results[$pre.'username']))
|
||||
{
|
||||
$results[$pre.'username'] = $row['cred_username'];
|
||||
$results[$pre.'password'] = '**oauth**';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached access-token, or use refresh-token to get a new one
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $refresh_token
|
||||
* @return string|null
|
||||
*/
|
||||
static protected function getAccessToken($username, $refresh_token)
|
||||
{
|
||||
return Api\Cache::getInstance(__CLASS__, 'access-token-'.$username.'-'.md5($refresh_token), static function() use ($username, $refresh_token)
|
||||
{
|
||||
if (!($oidc = Api\Auth\OpenIDConnectClient::byDomain($username)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
$token = $oidc->refreshToken($refresh_token);
|
||||
return $token->access_token;
|
||||
}
|
||||
catch (OpenIDConnectClientException $e) {
|
||||
_egw_log_exception($e);
|
||||
}
|
||||
return null;
|
||||
}, [], 3500); // access-token have a livetime of 3600s, give it some margin
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate username according to acc_imap_logintype and fetch password from session
|
||||
*
|
||||
|
@ -52,6 +52,8 @@ use Horde_Imap_Client_Mailbox_List;
|
||||
* @property-read boolean $acc_imap_administration enable administration
|
||||
* @property-read string $acc_imap_admin_username
|
||||
* @property-read string $acc_imap_admin_password
|
||||
* @property-read string $acc_oauth_username
|
||||
* @property-read string $acc_oauth_access_token
|
||||
* @property-read boolean $acc_further_identities are non-admin users allowed to create further identities
|
||||
* @property-read boolean $acc_user_editable are non-admin users allowed to edit this account, if it is for them
|
||||
* @property-read array $params parameters passed to constructor (all above as array)
|
||||
@ -180,6 +182,12 @@ class Imap extends Horde_Imap_Client_Socket implements Imap\PushIface
|
||||
'timeout' => $_timeout,
|
||||
)+self::$default_params;
|
||||
|
||||
// if we have an OAuth access-token for the user, pass it to the imap-client
|
||||
if (!$_adminConnection && !empty($this->params['acc_oauth_access_token']) && $parent_params['username'] === $this->params['acc_oauth_username'])
|
||||
{
|
||||
$parent_params['xoauth2_token'] = new \Horde_Imap_Client_Password_Xoauth2($parent_params['username'], $this->acc_oauth_access_token);
|
||||
}
|
||||
|
||||
if ($parent_params['cache'] === true)
|
||||
{
|
||||
$parent_params['cache'] = array(
|
||||
|
Loading…
Reference in New Issue
Block a user