WIP OAuth authentication for mail: working now with Gmail

This commit is contained in:
ralf 2022-12-25 14:49:37 -06:00
parent f85aa4dcbb
commit 14b6a9a5ab
9 changed files with 248 additions and 97 deletions

View File

@ -153,23 +153,6 @@ class admin_mail
*/ */
public static $no_sieve_blacklist = array('gmail.com', 'googlemail.com', 'outlook.office365.com'); 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 * Is current use a mail administrator / has run rights for EMailAdmin
* *
@ -223,31 +206,6 @@ class admin_mail
), $readonlys, $content, 2); ), $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 * Try to autoconfig an account
* *
@ -270,7 +228,21 @@ class admin_mail
{ {
$content['acc_imap_username'] = $content['ident_email']; $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); $hosts = array($content['acc_imap_host'] => true);
if ($content['acc_imap_port'] > 0 && !in_array($content['acc_imap_port'], array(143,993))) 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'])) elseif (($ispdb = self::mozilla_ispdb($content['ident_email'])) && count($ispdb['imap']))
{ {
$content['ispdb'] = $ispdb; $content['ispdb'] = $ispdb;
@ -324,7 +283,7 @@ class admin_mail
} }
// check if support OAuth for that domain or we have a password // 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!')); Etemplate::set_validation_error('acc_imap_password', lang('Field must not be empty!'));
$connected = false; $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['smtp_output'] .= "\n".Api\DateTime::to('now', 'H:i:s').": Trying $ssl connection to $host:$port ...\n";
$content['acc_smtp_port'] = $port; $content['acc_smtp_port'] = $port;
$mail = new Horde_Mail_Transport_Smtphorde($params=array( $params = [
'username' => $content['acc_smtp_username'], 'username' => $content['acc_smtp_username'],
'password' => $content['acc_smtp_password'], 'password' => $content['acc_smtp_password'],
'host' => $content['acc_smtp_host'], 'host' => $content['acc_smtp_host'],
@ -781,7 +740,12 @@ class admin_mail
'secure' => self::$ssl2secure[(string)array_search($content['acc_smtp_ssl'], self::$ssl2type)], 'secure' => self::$ssl2secure[(string)array_search($content['acc_smtp_ssl'], self::$ssl2type)],
'timeout' => self::TIMEOUT, 'timeout' => self::TIMEOUT,
'debug' => self::DEBUG_LOG, '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 // create smtp connection and authenticate, if credentials given
$smtp = $mail->getSMTPObject(); $smtp = $mail->getSMTPObject();
$content['smtp_output'] .= "\n".lang('Successful connected to %1 server%2.', 'SMTP', $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'])) if (!empty($content['acc_oauth_provider_url']))
{ {
$config['xoauth2_token'] = self::oauthToken($content); $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); return new Horde_Imap_Client_Socket($config);
} }
@ -1511,7 +1475,7 @@ class admin_mail
/** /**
* Acquire OAuth access (and refresh) token * 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'])) if (empty($content['acc_oauth_client_secret']))
{ {
@ -1521,9 +1485,14 @@ class admin_mail
$content['acc_oauth_client_id'], $content['acc_oauth_client_secret']); $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) // 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! // 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']); $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']);
} }
// Run OAuth authentification, will NOT return, but call success or failure callbacks below return new Horde_Imap_Client_Password_Xoauth2($content['acc_oauth_username'] ?? $content['acc_imap_username'], $content['acc_oauth_access_token']);
}
// Run OAuth authentication, will NOT return, but call success or failure callbacks below
$oidc->authenticateThen(__CLASS__.'::oauthAuthenticated', [$content], __CLASS__.'::oauthFailure', [$content]); $oidc->authenticateThen(__CLASS__.'::oauthAuthenticated', [$content], __CLASS__.'::oauthFailure', [$content]);
} }
@ -1552,8 +1525,16 @@ class admin_mail
*/ */
public static function oauthAuthenticated(OpenIDConnectClient $oidc, array $content) 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_access_token'] = $oidc->getAccessToken();
$content['acc_oauth_refresh_token'] = $oidc->getRefreshToken();
$GLOBALS['egw_info']['flags']['currentapp'] = 'admin'; $GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
@ -1570,6 +1551,7 @@ class admin_mail
public static function oauthFailure(Throwable $exception=null, array $content) public static function oauthFailure(Throwable $exception=null, array $content)
{ {
$content['output'] .= lang('OAuth Authentiction').': '.($exception ? $exception->getMessage() : lang('failed')); $content['output'] .= lang('OAuth Authentiction').': '.($exception ? $exception->getMessage() : lang('failed'));
$content['connected'] = false;
$GLOBALS['egw_info']['flags']['currentapp'] = 'admin'; $GLOBALS['egw_info']['flags']['currentapp'] = 'admin';

View File

@ -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 server hostname or ip address admin de LDAP-Server Hostname oder IP-Adresse
ldap settings admin de LDAP-Einstellungen ldap settings admin de LDAP-Einstellungen
leave empty for no quota admin de leer lassen um Quota zu deaktivieren 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 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 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 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 matches found admin de Keine Übereinstimmungen gefunden
no message returned. admin de Keine Nachricht zurückgeliefert. no message returned. admin de Keine Nachricht zurückgeliefert.
no modes available admin de Kein Modus verfügbar 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 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 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 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 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 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 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 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 official egroupware usage statistic admin de Offizielle EGroupware Nutzungsstatistik
one day admin de ein Tag one day admin de ein Tag
@ -861,6 +864,7 @@ subtype admin de Untertyp
subversion checkout admin de Subversion checkout (svn) subversion checkout admin de Subversion checkout (svn)
success admin de erfolgreich success admin de erfolgreich
successful connected to %1 server%2. admin de Erfolgreich zu %1 Server verbunden%2. 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 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 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 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 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 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 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 cookies to pass sessionid admin de Sitzungs-ID in einem Cookie speichern
use default admin de Vorgabe verwenden use default admin de Vorgabe verwenden
use ldap defaults admin de LDAP Standardeinstellungen benutzen 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 can utilize these stationery templates admin de Benutzer können diese Briefpapiervorlagen verwenden
users choice admin de Benutzerauswahl users choice admin de Benutzerauswahl
using data from mozilla ispdb for provider %1 admin de Benutzer Mozilla ISPDB für Provider %1 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 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 vacation notice admin de Abwesenheitsnotiz
value for column %1 is not unique! admin de Wert für die Spalte %1 ist nicht eindeutig! value for column %1 is not unique! admin de Wert für die Spalte %1 ist nicht eindeutig!

View File

@ -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 server hostname or ip address admin en LDAP server host name or IP address
ldap settings admin en LDAP settings ldap settings admin en LDAP settings
leave empty for no quota admin en Leave empty for no quota 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 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 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 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 matches found admin en No matches found!
no message returned. admin en No message returned no message returned. admin en No message returned
no modes available admin en No modes available! 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 groups admin en No permission to add groups!
no permission to add users admin en No permission to add users! no permission to add users admin en No permission to add users!
no permission to create groups admin en No permission to create groups! 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 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. 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 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 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 official egroupware usage statistic admin en Official EGroupware usage statistic
one day admin en One day one day admin en One day
@ -864,6 +867,7 @@ subtype admin en Sub type
subversion checkout admin en Subversion checkout subversion checkout admin en Subversion checkout
success admin en Success success admin en Success
successful connected to %1 server%2. admin en Successful connected to %1 server%2. 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 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 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 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 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 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 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 cookies to pass sessionid admin en Use cookies to pass session ID
use default admin en use default use default admin en use default
use ldap defaults admin en Use LDAP defaults 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 can utilize these stationery templates admin en Users can utilize these stationery templates
users choice admin en Users choice 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 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 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 vacation notice admin en Vacation notice
value for column %1 is not unique! admin en Value for column %1 is not unique! value for column %1 is not unique! admin en Value for column %1 is not unique!

View File

@ -67,7 +67,7 @@
</row> </row>
<row> <row>
<description for="acc_imap_password" value="Password"/> <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 id="acc_imap_account_id" class="emailadmin_diagnostic"/>
<description/> <description/>
</row> </row>
@ -220,7 +220,7 @@
<row class="@manual_class"> <row class="@manual_class">
<description for="acc_smtp_password" value="Password"/> <description for="acc_smtp_password" value="Password"/>
<hbox> <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"/> <description id="acc_smtp_account_id" class="emailadmin_diagnostic"/>
</hbox> </hbox>
</row> </row>

View File

@ -23,7 +23,7 @@
</row> </row>
<row> <row>
<description value="Password" for="acc_imap_password"/> <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>
<row class="@manual_class"> <row class="@manual_class">
<description value="IMAP server" for="acc_imap_host"/> <description value="IMAP server" for="acc_imap_host"/>

View File

@ -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: * 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 { * 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 302 https://$redirect_host/$redirect_path/api/oauth.php?$args;
* } * }
* return 301 https://github.com/EGroupware/egroupware/blob/master/api/src/Auth/OpenIDConnectClient.php; * 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> * 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://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 class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient
{ {
const EGROUPWARE_OAUTH_PROXY = 'https://proxy.egroupware.org/oauth'; 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) public function __construct($provider_url = null, $client_id = null, $client_secret = null, $issuer = null)
{ {
parent::__construct($provider_url, $client_id, $client_secret, $issuer); 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 // 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); $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;
} }
/** /**

View File

@ -555,7 +555,7 @@ class Account implements \ArrayAccess
// Horde use locale for translation of error messages // Horde use locale for translation of error messages
Api\Preferences::setlocale(LC_MESSAGES); Api\Preferences::setlocale(LC_MESSAGES);
$this->smtpTransport = new Horde_Mail_Transport_Smtphorde(array( $config = [
'username' => $params['acc_smtp_username'] ?? null, 'username' => $params['acc_smtp_username'] ?? null,
'password' => $params['acc_smtp_password'] ?? null, 'password' => $params['acc_smtp_password'] ?? null,
'host' => $params['acc_smtp_host'], 'host' => $params['acc_smtp_host'],
@ -563,7 +563,13 @@ class Account implements \ArrayAccess
'secure' => $secure, 'secure' => $secure,
'debug' => self::SMTP_DEBUG_LOG, 'debug' => self::SMTP_DEBUG_LOG,
//'timeout' => self::TIMEOUT, //'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; return $this->smtpTransport;
} }
@ -1305,6 +1311,14 @@ class Account implements \ArrayAccess
} }
// check for whom we have to store credentials // check for whom we have to store credentials
$valid_for = self::credentials_valid_for($data, $user); $valid_for = self::credentials_valid_for($data, $user);
// 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 // add imap credentials
$cred_type = $data['acc_imap_username'] == $data['acc_smtp_username'] && $cred_type = $data['acc_imap_username'] == $data['acc_smtp_username'] &&
$data['acc_imap_password'] == $data['acc_smtp_password'] ? 3 : 1; $data['acc_imap_password'] == $data['acc_smtp_password'] ? 3 : 1;
@ -1329,6 +1343,7 @@ class Account implements \ArrayAccess
{ {
Credentials::delete($data['acc_id'], $valid_for, Credentials::SMTP, true); Credentials::delete($data['acc_id'], $valid_for, Credentials::SMTP, true);
} }
}
// store or delete admin credentials // store or delete admin credentials
if ($data['acc_imap_admin_username'] && $data['acc_imap_admin_password']) if ($data['acc_imap_admin_username'] && $data['acc_imap_admin_password'])

View File

@ -15,6 +15,7 @@
namespace EGroupware\Api\Mail; namespace EGroupware\Api\Mail;
use EGroupware\Api; use EGroupware\Api;
use Jumbojett\OpenIDConnectClientException;
/** /**
* Mail account credentials are stored in egw_ea_credentials for given * Mail account credentials are stored in egw_ea_credentials for given
@ -79,10 +80,15 @@ class Credentials
*/ */
const SPAMTITAN = 128; const SPAMTITAN = 128;
/**
* Refresh token for IMAP & SMTP via OAuth
*/
const OAUTH_REFRESH_TOKEN = 256;
/** /**
* All credentials * 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 * Password in cleartext
@ -132,6 +138,7 @@ class Credentials
self::SMIME => 'acc_smime_', self::SMIME => 'acc_smime_',
self::TWOFA => '2fa_', self::TWOFA => '2fa_',
self::SPAMTITAN => 'acc_spam_', self::SPAMTITAN => 'acc_spam_',
self::OAUTH_REFRESH_TOKEN => 'acc_oauth_'
); );
/** /**
@ -183,8 +190,8 @@ class Credentials
'account_id' => $account_id, 'account_id' => $account_id,
'(cred_type & '.(int)$type.') > 0', // postgreSQL require > 0, or gives error as it expects boolean '(cred_type & '.(int)$type.') > 0', // postgreSQL require > 0, or gives error as it expects boolean
), __LINE__, __FILE__, false, ), __LINE__, __FILE__, false,
// account_id DESC ensures 0=all allways overwrite (old user-specific credentials) // account_id DESC ensures 0=all always overwrite (old user-specific credentials)
'ORDER BY account_id ASC', self::APP); 'ORDER BY account_id ASC, cred_type ASC', self::APP);
//error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") nothing in cache"); //error_log(__METHOD__."($acc_id, $type, ".array2string($account_id).") nothing in cache");
} }
else else
@ -230,12 +237,56 @@ class Credentials
$results[$prefix.'cred_id'] = $row['cred_id']; $results[$prefix.'cred_id'] = $row['cred_id'];
$results[$prefix.'account_id'] = $row['account_id']; $results[$prefix.'account_id'] = $row['account_id'];
$results[$prefix.'pw_enc'] = $row['cred_pw_enc']; $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; 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 * Generate username according to acc_imap_logintype and fetch password from session
* *

View File

@ -52,6 +52,8 @@ use Horde_Imap_Client_Mailbox_List;
* @property-read boolean $acc_imap_administration enable administration * @property-read boolean $acc_imap_administration enable administration
* @property-read string $acc_imap_admin_username * @property-read string $acc_imap_admin_username
* @property-read string $acc_imap_admin_password * @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_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 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) * @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, 'timeout' => $_timeout,
)+self::$default_params; )+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) if ($parent_params['cache'] === true)
{ {
$parent_params['cache'] = array( $parent_params['cache'] = array(