* Mail: OAuth authentication for Microsoft (Office365, outlook.com, ...) and GMail

This commit is contained in:
ralf 2023-01-16 16:56:30 -06:00
parent 8a43d88ffe
commit c49f7849bb
6 changed files with 69 additions and 30 deletions

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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';
}

View File

@ -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')
{