Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions config/hooks.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,60 @@ class IMP_Hooks
// return $credentials;
// }

// =========================================================================
// OIDC / XOAUTH2 hooks
// =========================================================================
// These hooks enable XOAUTH2 authentication for IMAP when the user has
// logged in via an OIDC provider (e.g. Apereo CAS, Keycloak).
// Requires horde/Core >= 6.x with OAuthTokenService configured.
//
// To activate: uncomment and place in var/config/imp/hooks.php
// =========================================================================

/**
* imap_preauthenticate
*
* Called by IMP before opening an IMAP connection.
* Injects a Horde_Imap_Client_Password_Xoauth2 object if the user has
* OAuth2 tokens stored. Falls back to password auth if not.
*
* @param array $credentials Current credentials array
* @return array Modified credentials
*/
// public function imap_preauthenticate(array $credentials): array
// {
// global $injector;
//
// $username = $injector->getInstance('Horde_Registry')->getAuth();
// if (!$username) {
// return $credentials;
// }
//
// $tokenService = $injector->getInstance(\Horde\Core\Service\OAuthTokenService::class);
// $providerConfig = $injector->getInstance(\Horde\Core\Service\OAuthProviderConfigRepository::class);
//
// $row = \Horde\Core\Service\OidcHookHelper::findProviderForUser(
// $username, $tokenService, $providerConfig
// );
// if ($row === null) {
// return $credentials;
// }
//
// $accessToken = \Horde\Core\Service\OidcHookHelper::getValidAccessToken(
// $username, $row, $tokenService, $injector
// );
// if ($accessToken === null) {
// return $credentials;
// }
//
// $xoauth2User = \Horde\Core\Service\OidcHookHelper::xoauth2Username($username, $row);
// $credentials['password'] = new Horde_Imap_Client_Password_Xoauth2(
// $xoauth2User,
// $accessToken
// );
//
// return $credentials;
// }

/**
* PREFERENCE INIT: Set preference values on login.
Expand Down Expand Up @@ -522,6 +576,42 @@ class IMP_Hooks
*/
public function dynamic_prefs()
{
// ---------------------------------------------------------------
// OIDC/XOAUTH2: proactive token refresh
// Uncomment to refresh OAuth2 tokens before they expire in IMP.
// Requires horde/Core >= 6.x with OAuthTokenService configured.
// ---------------------------------------------------------------
// global $injector;
// $tokenService = $injector->getInstance(\Horde\Core\Service\OAuthTokenService::class);
// $providerConfig = $injector->getInstance(\Horde\Core\Service\OAuthProviderConfigRepository::class);
// $username = $injector->getInstance('Horde_Registry')->getAuth();
// if ($username) {
// $row = \Horde\Core\Service\OidcHookHelper::findProviderForUser(
// $username, $tokenService, $providerConfig
// );
// if ($row !== null) {
// try {
// $tokenSet = $tokenService->getTokenSet($username, $row['provider_id']);
// if ($tokenSet->isExpired(300)) {
// $accessToken = \Horde\Core\Service\OidcHookHelper::getValidAccessToken(
// $username, $row, $tokenService, $injector
// );
// if ($accessToken !== null) {
// $xoauth2User = \Horde\Core\Service\OidcHookHelper::xoauth2Username($username, $row);
// $xoauth2Obj = new Horde_Imap_Client_Password_Xoauth2($xoauth2User, $accessToken);
// $session = $injector->getInstance('Horde_Session');
// foreach (array_keys($_SESSION['imp'] ?? []) as $key) {
// if (str_starts_with($key, 'IMP_Imap_Password/')) {
// $session->set('imp', $key, $xoauth2Obj, $session::ENCRYPT);
// }
// }
// }
// }
// } catch (\Throwable $e) {}
// }
// }
// ---------------------------------------------------------------

return array(
/* Preview pane. Valid values:
* - 'horiz': Horizontal mode
Expand Down
49 changes: 45 additions & 4 deletions lib/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class IMP_Auth
* - password: (string) The user password.
* - server: (string) The server key to use (from backends.php).
* - userId: (string) The username.
* - xoauth2_token: (Horde_Imap_Client_Password_Xoauth2) XOAUTH2 token
* (alternative to password, used with OIDC auth).
*
* @throws Horde_Auth_Exception
*/
Expand All @@ -59,7 +61,8 @@ public static function authenticate($credentials = [])
// Check for valid IMAP Client object.
if (!$imp_imap->init) {
if (!isset($credentials['userId'])
|| !isset($credentials['password'])) {
|| (!isset($credentials['password'])
&& !isset($credentials['xoauth2_token']))) {
throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
}

Expand All @@ -74,7 +77,11 @@ public static function authenticate($credentials = [])
}

try {
$imp_imap->createBaseImapObject($credentials['userId'], $credentials['password'], $credentials['server']);
$imp_imap->createBaseImapObject(
$credentials['userId'],
$credentials['xoauth2_token'] ?? $credentials['password'],
$credentials['server']
);
} catch (IMP_Imap_Exception $e) {
self::_log(false, $imp_imap);
throw $e->authException();
Expand Down Expand Up @@ -112,8 +119,8 @@ public static function transparent($auth_ob)
$credentials['userId'] = $auth_ob->getCredential('userId');
}

if (!isset($credentials['password'])
|| !strlen($credentials['password'])) {
if (empty($credentials['xoauth2_token'])
&& empty($credentials['password'])) {
return false;
}

Expand Down Expand Up @@ -204,6 +211,40 @@ protected static function _canAutoLogin($server_key = null, $force = false)
{
global $injector, $registry;

// OIDC/XOAUTH2: only if auth driver is oidc.
// Must come before loadServerConfig() which may fail when IMP is not
// fully initialised (e.g. portal rendering before IMP appInit).
if (!empty($GLOBALS['conf']['auth']['driver'])
&& strcasecmp($GLOBALS['conf']['auth']['driver'], 'oidc') === 0) {
$username = $registry->getAuth();
if ($username) {
$tokenService = $injector->getInstance(\Horde\Core\Service\OAuthTokenService::class);
$providerConfig = $injector->getInstance(\Horde\Core\Service\OAuthProviderConfigRepository::class);
$row = \Horde\Core\Service\OidcHookHelper::findProviderForUser(
$username, $tokenService, $providerConfig
);
if ($row !== null) {
$accessToken = \Horde\Core\Service\OidcHookHelper::getValidAccessToken(
$username, $row, $tokenService, $injector
);
if ($accessToken !== null) {
$xoauth2User = \Horde\Core\Service\OidcHookHelper::xoauth2Username(
$username, $row
);
return [
'userId' => $xoauth2User,
'xoauth2_token' => new Horde_Imap_Client_Password_Xoauth2(
$xoauth2User, $accessToken
),
'server' => $server_key ?? self::getAutoLoginServer(),
];
}
}
}
// OIDC driver but no tokens available — do not fall through to hordeauth
return false;
}

if (($servers = $injector->getInstance('IMP_Factory_Imap')->create()->loadServerConfig()) === false) {
return false;
}
Expand Down
Loading