From c49f7849bbdc761eeff42a3ad44ee37f31bf8361 Mon Sep 17 00:00:00 2001 From: ralf Date: Mon, 16 Jan 2023 16:56:30 -0600 Subject: [PATCH] * Mail: OAuth authentication for Microsoft (Office365, outlook.com, ...) and GMail --- admin/inc/class.admin_mail.inc.php | 81 +++++++++++++++++++--------- admin/lang/egw_de.lang | 1 + admin/lang/egw_en.lang | 1 + api/src/Auth/OpenIDConnectClient.php | 5 +- api/src/Framework/Extra.php | 2 +- api/src/Mail/Account.php | 9 +++- 6 files changed, 69 insertions(+), 30 deletions(-) diff --git a/admin/inc/class.admin_mail.inc.php b/admin/inc/class.admin_mail.inc.php index cfe433c710..657f3e52fd 100644 --- a/admin/inc/class.admin_mail.inc.php +++ b/admin/inc/class.admin_mail.inc.php @@ -1243,7 +1243,7 @@ class admin_mail $sel_options['acc_smtp_ssl'] = self::$ssl_types; // admin access to account with no credentials available - if ($this->is_admin && (empty($content['acc_imap_username']) || empty($content['acc_imap_host']) || $content['called_for'])) + if ($this->is_admin && (!empty($content['called_for']) || empty($content['acc_imap_host']) || $content['called_for'])) { // can't connection to imap --> allow free entries in taglists foreach(array('acc_folder_sent', 'acc_folder_trash', 'acc_folder_draft', 'acc_folder_template', 'acc_folder_junk') as $folder) @@ -1254,6 +1254,11 @@ class admin_mail else { try { + if (empty($content['acc_imap_username']) && ($oauth = OpenIDConnectClient::providerByDomain( + $content['acc_oauth_username'] ?? $content['acc_imap_username'] ?? $content['ident_email'], $content['acc_imap_host']))) + { + $content += self::oauth2content($oauth); + } $sel_options['acc_folder_sent'] = $sel_options['acc_folder_trash'] = $sel_options['acc_folder_draft'] = $sel_options['acc_folder_template'] = $sel_options['acc_folder_junk'] = $sel_options['acc_folder_archive'] = @@ -1482,9 +1487,10 @@ class admin_mail 'timeout' => $timeout > 0 ? $timeout : Mail\Imap::getTimeOut(), 'debug' => self::DEBUG_LOG, ]; - if (!empty($content['acc_oauth_provider_url'])) + if (!empty($content['acc_oauth_provider_url']) || !empty($content['acc_oauth_access_token'])) { $config['xoauth2_token'] = self::oauthToken($content); + $config['username'] = $content['acc_oauth_username'] ?? $content['acc_imap_username']; if (empty($config['password'])) $config['password'] = '**oauth**'; // some password is required, even if not used } return new Horde_Imap_Client_Socket($config); @@ -1495,28 +1501,36 @@ class admin_mail */ protected static function oauthToken(array &$content, bool $smtp=false) { - if (empty($content['acc_oauth_client_secret'])) + if (empty($content['acc_oauth_access_token'])) { - throw new Exception(lang("No OAuth client secret for provider '%1'!", $content['acc_oauth_provider_url'])); - } - $oidc = new OpenIDConnectClient($content['acc_oauth_provider_url'], - $content['acc_oauth_client_id'], $content['acc_oauth_client_secret']); + if (empty($content['acc_oauth_client_secret']) && + ($oauth = OpenIDConnectClient::providerByDomain($content['acc_oauth_username'] ?? $content['acc_imap_username'] ?? $content['ident_email'], $content['acc_imap_host']))) + { + $content += self::oauth2content($oauth); + } + if (empty($content['acc_oauth_client_secret'])) + { + throw new Exception(lang("No OAuth client secret for provider '%1'!", $content['acc_oauth_provider_url'])); + } + $oidc = new OpenIDConnectClient($content['acc_oauth_provider_url'], + $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[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN])) - { - $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(str_replace('$username', $content['acc_oauth_username'] ?? $content['acc_imap_username'], $content[OpenIDConnectClient::ADD_AUTH_PARAM])); - } + // Office365 requires client-ID as appid GET parameter (https://github.com/jumbojett/OpenID-Connect-PHP/issues/190) + if (!empty($content[OpenIDConnectClient::ADD_CLIENT_TO_WELL_KNOWN])) + { + $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(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! - $oidc->setResponseTypes(['code']); // to be able to use query, not 'id_token' - //$oidc->setAllowImplicitFlow(true); - $oidc->addScope($content['acc_oauth_scopes']); + // 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' + //$oidc->setAllowImplicitFlow(true); + $oidc->addScope($content['acc_oauth_scopes']); + } if (!empty($content['acc_oauth_access_token']) || !empty($content['acc_oauth_refresh_token'])) { @@ -1545,7 +1559,7 @@ class admin_mail { if (empty($content['acc_oauth_username'])) { - $content['acc_oauth_username'] = $content['acc_imap_username']; + $content['acc_oauth_username'] = $content['acc_imap_username'] ?? $oidc->getVerifiedClaims('email') ?? $content['ident_email']; } if (empty($content['acc_oauth_refresh_token'] = $oidc->getRefreshToken())) { @@ -1557,7 +1571,14 @@ class admin_mail $GLOBALS['egw_info']['flags']['currentapp'] = 'admin'; $obj = new self; - $obj->autoconfig($content); + if (!empty($content['acc_id'])) + { + $obj->edit($content, lang('Use save or apply to store the received OAuth token!'), 'info'); + } + else + { + $obj->autoconfig($content); + } } /** @@ -1568,12 +1589,20 @@ 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'; $obj = new self; + if (!empty($content['acc_id'])) + { + $obj->edit($content, lang('OAuth Authentiction').': '.($exception ? $exception->getMessage() : lang('failed')), 'error'); + } + else + { + $content['output'] .= lang('OAuth Authentiction').': '.($exception ? $exception->getMessage() : lang('failed')); + $content['connected'] = false; + + $obj->autoconfig($content); + } $obj->autoconfig($content); } diff --git a/admin/lang/egw_de.lang b/admin/lang/egw_de.lang index 416105f742..fcdd97d31d 100644 --- a/admin/lang/egw_de.lang +++ b/admin/lang/egw_de.lang @@ -949,6 +949,7 @@ use default admin de Vorgabe verwenden use ldap defaults admin de LDAP Standardeinstellungen benutzen use predefined username and password defined below admin de Verwende den unten vordefinierten Benutzernamen und Passwort use pure html compliant code (not fully working yet) admin de Vollständig HTML kompatiblen Code verwenden (nicht vollständig implementiert) +use save or apply to store the received oauth token! admin de Benutze Speicher oder Übernehmen um das erhaltene OAuth Token zu speichern! use secure cookies (transmitted only via https) admin de Benutze sichere Cookies (werden nur per https übertragen) use smtp auth admin de SMTP Authentifizierung benutzen use theme admin de Benutztes Farbschema diff --git a/admin/lang/egw_en.lang b/admin/lang/egw_en.lang index 2ea3c876f7..91fc2ce6b5 100644 --- a/admin/lang/egw_en.lang +++ b/admin/lang/egw_en.lang @@ -952,6 +952,7 @@ use default admin en use default use ldap defaults admin en Use LDAP defaults use predefined username and password defined below admin en Use predefined username and password defined below use pure html compliant code (not fully working yet) admin en Use pure HTML compliant code +use save or apply to store the received oauth token! admin en Use save or apply to store the received OAuth token! use secure cookies (transmitted only via https) admin en Use secure cookies (transmitted only via https) use smtp auth admin en Use SMTP authentication use theme admin en Use theme diff --git a/api/src/Auth/OpenIDConnectClient.php b/api/src/Auth/OpenIDConnectClient.php index 020a8b57f7..4345cce280 100644 --- a/api/src/Auth/OpenIDConnectClient.php +++ b/api/src/Auth/OpenIDConnectClient.php @@ -122,11 +122,12 @@ class OpenIDConnectClient extends \Jumbojett\OpenIDConnectClient * Get OIDC client object for the given domain/email * * @param string $domain domain or email address + * @param string|null $mailserver * @return self|null */ - public static function byDomain($domain) + public static function byDomain($domain, $mailserver=null) { - if (!($provider = self::providerByDomain($domain))) + if (!($provider = self::providerByDomain($domain, $mailserver))) { return null; } diff --git a/api/src/Framework/Extra.php b/api/src/Framework/Extra.php index 893317bd9f..ec1a95b389 100644 --- a/api/src/Framework/Extra.php +++ b/api/src/Framework/Extra.php @@ -59,7 +59,7 @@ abstract class Extra // if we have real push available and a regular single-entry refresh of a push supporting app, no need to refresh if (!Json\Push::onlyFallback() && !empty($type) && !empty($id) && // $type === null --> full reload - Link::get_registry($app, 'push_data') !== null) + Link::get_registry($app, 'push_data')) { $app = 'msg-only-push-refresh'; } diff --git a/api/src/Mail/Account.php b/api/src/Mail/Account.php index e4b4eb3312..6e8a7d1b47 100644 --- a/api/src/Mail/Account.php +++ b/api/src/Mail/Account.php @@ -469,10 +469,17 @@ class Account implements \ArrayAccess */ public function is_imap($try_connect=true) { - if (empty($this->acc_imap_host) || ( empty($this->acc_imap_username) && empty($this->acc_imap_password) ) ) + if (empty($this->acc_imap_host) || + empty($this->acc_imap_username) && empty($this->acc_imap_password) && + !($oauth = Api\Auth\OpenIDConnectClient::providerByDomain($this->acc_imap_username ?: $this->ident_email, $this->acc_imap_host))) { return false; // no imap host or credentials } + if (isset($oauth)) + { + $this->params['acc_imap_username'] = $this->acc_imap_username ?: $this->ident_email; + $this->params['acc_imap_password'] = '**oauth**'; + } // if we are not managing the mail-server, we do NOT need to check deliveryMode and accountStatus if ($this->acc_smtp_type == __NAMESPACE__.'\\Smtp') {