From fd71c2d8f23a22017d498aa988b10ff82a040d09 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 14 Feb 2024 22:03:58 +0100 Subject: [PATCH 1/5] #275 2FA login --- lam/lib/selfService.inc | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lam/lib/selfService.inc b/lam/lib/selfService.inc index c6568155f..49f92d218 100644 --- a/lam/lib/selfService.inc +++ b/lam/lib/selfService.inc @@ -925,6 +925,7 @@ class selfServiceProfile { public function getLoginHandler(): SelfServiceLoginHandler { return match ($this->loginHandler) { SelfServiceHttpAuthLoginHandler::ID => new SelfServiceHttpAuthLoginHandler($this), + SelfService2FaLoginHandler::ID => new SelfService2FaLoginHandler($this), default => new SelfServiceUserPasswordLoginHandler($this), }; } @@ -1143,3 +1144,51 @@ class SelfServiceHttpAuthLoginHandler implements SelfServiceLoginHandler { } } + +/** + * Performs login with pure 2FA. + */ +class SelfService2FaLoginHandler implements SelfServiceLoginHandler { + + public const ID = "2fa"; + + private selfServiceProfile $profile; + + /** + * Constructor + * + * @param selfServiceProfile $profile profile + */ + function __construct(selfServiceProfile $profile) { + $this->profile = $profile; + } + + /** + * @inheritDoc + */ + function getId(): string { + return self::ID; + } + + /** + * @inheritDoc + */ + function addLoginFields(htmlResponsiveRow $content): void { + // no input fields + } + + /** + * @inheritDoc + */ + function getLoginName(): string { + return ''; + } + + /** + * @inheritDoc + */ + function getLoginPassword(): string { + return ''; + } + +} From 48c9487f943b6765bc41fb6ebbb83450da4148c5 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 15 Feb 2024 20:04:14 +0100 Subject: [PATCH 2/5] #275 2FA login --- lam/lib/selfService.inc | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/lam/lib/selfService.inc b/lam/lib/selfService.inc index 49f92d218..63f7013a4 100644 --- a/lam/lib/selfService.inc +++ b/lam/lib/selfService.inc @@ -1021,6 +1021,22 @@ interface SelfServiceLoginHandler { */ function getLoginPassword(): string; + /** + * Returns if the login handler manages authentication on its own. + * + * @return bool manages authentication + */ + function managesAuthentication(): bool; + + /** + * Returns if the authentication was successful. + * Only valid if managesAuthentication() returns true. + * + * @return bool authentication successful + * @throws LAMException error during authentication + */ + function isAuthenticationSuccessful(): bool; + } /** @@ -1082,6 +1098,20 @@ class SelfServiceUserPasswordLoginHandler implements SelfServiceLoginHandler { return $_POST['password']; } + /** + * @inheritDoc + */ + function managesAuthentication(): bool { + return false; + } + + /** + * @inheritDoc + */ + function isAuthenticationSuccessful(): bool { + throw new LAMException('Not implemented'); + } + } /** @@ -1143,6 +1173,20 @@ class SelfServiceHttpAuthLoginHandler implements SelfServiceLoginHandler { return $_SERVER['PHP_AUTH_PW']; } + /** + * @inheritDoc + */ + function managesAuthentication(): bool { + return false; + } + + /** + * @inheritDoc + */ + function isAuthenticationSuccessful(): bool { + throw new LAMException('Not implemented'); + } + } /** @@ -1191,4 +1235,28 @@ class SelfService2FaLoginHandler implements SelfServiceLoginHandler { return ''; } + /** + * @inheritDoc + */ + function managesAuthentication(): bool { + return true; + } + + /** + * @inheritDoc + */ + function isAuthenticationSuccessful(): bool { + if (($this->profile->twoFactorAuthentication !== TwoFactorProviderService::TWO_FACTOR_OKTA) + && ($this->profile->twoFactorAuthentication !== TwoFactorProviderService::TWO_FACTOR_OPENID)) { + logNewMessage(LOG_ERR, 'Unsupported 2FA provider: ' . $this->profile->twoFactorAuthentication); + return false; + } + if (!$this->profile->useForAllOperations) { + logNewMessage(LOG_ERR, 'Use for all operations must be set'); + return false; + } + // authentication will be checked on 2FA page + return true; + } + } From 58f950c0936ec213427a4fd5fd234fd33e764883 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 16 Feb 2024 20:23:11 +0100 Subject: [PATCH 3/5] #275 2FA login --- lam/lib/2factor.inc | 29 ++++++++++++++----- lam/lib/selfService.inc | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/lam/lib/2factor.inc b/lam/lib/2factor.inc index 89a04b0ed..f9477d664 100644 --- a/lam/lib/2factor.inc +++ b/lam/lib/2factor.inc @@ -7,6 +7,7 @@ use Duo\DuoUniversal\DuoException; use Exception; use \htmlResponsiveRow; use \LAM\LOGIN\WEBAUTHN\WebauthnManager; +use SelfServiceLoginHandler; use \selfServiceProfile; use \LAMConfig; use \htmlScript; @@ -595,9 +596,11 @@ class OktaProvider extends BaseProvider { * @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput() */ public function addCustomInput(&$row, $userDn) { - $loginAttribute = $this->getLoginAttributeValue($userDn); - if (empty($loginAttribute)) { - throw new LAMException('Unable to read login attribute from ' . $userDn); + if (($this->config->loginHandler === null) || !$this->config->loginHandler->managesAuthentication()) { + $loginAttribute = $this->getLoginAttributeValue($userDn); + if (empty($loginAttribute)) { + throw new LAMException('Unable to read login attribute from ' . $userDn); + } } if ($this->verificationFailed) { return; @@ -656,10 +659,16 @@ class OktaProvider extends BaseProvider { try { $claims = json_decode(base64_decode(explode('.', $accessCode)[1]), true); logNewMessage(LOG_DEBUG, 'Okta claims: ' . print_r($claims, true)); - $loginAttribute = $this->getLoginAttributeValue($user); - if ($loginAttribute !== $claims['sub']) { - logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim sub: ' . $claims['sub']); - return false; + $oktaUser = $claims['sub']; + if (($this->config->loginHandler !== null) && $this->config->loginHandler->managesAuthentication()) { + $this->config->loginHandler->authorize2FaUser($oktaUser); + } + else { + $loginAttribute = $this->getLoginAttributeValue($user); + if ($loginAttribute !== $oktaUser) { + logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim sub: ' . $oktaUser); + return false; + } } $this->verificationFailed = false; return true; @@ -1250,6 +1259,7 @@ class TwoFactorProviderService { $tfConfig->twoFactorRememberDeviceDuration = $profile->twoFactorRememberDeviceDuration; $tfConfig->twoFactorRememberDevicePassword = $profile->twoFactorRememberDevicePassword; } + $tfConfig->loginHandler = $profile->getLoginHandler(); return $tfConfig; } @@ -1362,4 +1372,9 @@ class TwoFactorConfiguration { */ public string $twoFactorRememberDevicePassword = ''; + /** + * @var SelfServiceLoginHandler|null login handler + */ + public ?SelfServiceLoginHandler $loginHandler = null; + } diff --git a/lam/lib/selfService.inc b/lam/lib/selfService.inc index 63f7013a4..8a794e992 100644 --- a/lam/lib/selfService.inc +++ b/lam/lib/selfService.inc @@ -1037,6 +1037,14 @@ interface SelfServiceLoginHandler { */ function isAuthenticationSuccessful(): bool; + /** + * Authorizes an user that was provided by 2FA provider. + * + * @param string $userName user name + * @throws LAMException error during authentication + */ + function authorize2FaUser(string $userName): void; + } /** @@ -1112,6 +1120,13 @@ class SelfServiceUserPasswordLoginHandler implements SelfServiceLoginHandler { throw new LAMException('Not implemented'); } + /** + * @inheritDoc + */ + function authorize2FaUser(string $userName): void { + // no action + } + } /** @@ -1187,6 +1202,13 @@ class SelfServiceHttpAuthLoginHandler implements SelfServiceLoginHandler { throw new LAMException('Not implemented'); } + /** + * @inheritDoc + */ + function authorize2FaUser(string $userName): void { + // no action + } + } /** @@ -1259,4 +1281,44 @@ class SelfService2FaLoginHandler implements SelfServiceLoginHandler { return true; } + /** + * @inheritDoc + */ + function authorize2FaUser(string $userName): void { + $bindUser = $this->profile->LDAPUser; + if (empty($bindUser)) { + throw new LAMException('SelfService2FaLoginHandler', 'No bind user set'); + } + $bindPassword = deobfuscateText($this->profile->LDAPPassword); + $server = connectToLDAP($this->profile->serverURL, $this->profile->useTLS); + if ($server === null) { + throw new LAMException('SelfService2FaLoginHandler', 'Unable to match provided user with LDAP entry - LDAP connect failed'); + } + ldap_set_option($server, LDAP_OPT_REFERRALS, $this->profile->followReferrals); + $bind = @ldap_bind($server, $bindUser, $bindPassword); + if (!$bind) { + throw new LAMException('SelfService2FaLoginHandler', 'Unable to match provided user with LDAP entry - LDAP bind failed'); + } + $filter = '(' . $this->profile->twoFactorAuthenticationAttribute . "=" . ldap_escape($userName) . ')'; + if (!empty($profile->additionalLDAPFilter)) { + $filter = '(&' . $filter . $profile->additionalLDAPFilter . ')'; + } + $result = @ldap_search($server, $this->profile->LDAPSuffix, $filter, ['DN'], 0, 1, 0, LDAP_DEREF_NEVER); + if ($result === false) { + throw new LAMException('SelfService2FaLoginHandler', 'Unable to match provided user with LDAP entry - LDAP search failed'); + } + $entries = @ldap_get_entries($server, $result); + if ($entries === false) { + throw new LAMException('SelfService2FaLoginHandler', 'Unable to match provided user with LDAP entry - LDAP search failed'); + } + $info = $entries; + cleanLDAPResult($info); + if (sizeof($info) === 1) { + $userDN = $info[0]['dn']; + $_SESSION['selfService_clientDN'] = lamEncrypt($userDN, 'SelfService'); + return; + } + throw new LAMException('SelfService2FaLoginHandler', 'Multiple or no results for ' . $userName . ' ' . print_r($info, true)); + } + } From 190a976ede91030c8753aa8c8f1447af9db0014c Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 16 Feb 2024 20:39:16 +0100 Subject: [PATCH 4/5] #275 2FA login --- lam/lib/2factor.inc | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lam/lib/2factor.inc b/lam/lib/2factor.inc index f9477d664..652eccd4a 100644 --- a/lam/lib/2factor.inc +++ b/lam/lib/2factor.inc @@ -780,9 +780,12 @@ class OpenIdProvider extends BaseProvider { * @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput() */ public function addCustomInput(&$row, $userDn) { - $loginAttribute = $this->getLoginAttributeValue($userDn); - if (empty($loginAttribute)) { - throw new LAMException('Unable to read login attribute from ' . $userDn); + $loginAttribute = ''; + if (($this->config->loginHandler === null) || !$this->config->loginHandler->managesAuthentication()) { + $loginAttribute = $this->getLoginAttributeValue($userDn); + if (empty($loginAttribute)) { + throw new LAMException('Unable to read login attribute from ' . $userDn); + } } if ($this->verificationFailed) { return; @@ -876,10 +879,16 @@ class OpenIdProvider extends BaseProvider { $tokenSet = $authorizationService->callback($client, $callbackParams, $_GET['redirect_uri']); $claims = $tokenSet->claims(); logNewMessage(LOG_DEBUG, print_r($claims, true)); - $loginAttribute = $this->getLoginAttributeValue($user); - if ($loginAttribute !== $claims['preferred_username']) { - logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim preferred_username: ' . $claims['preferred_username']); - return false; + $openIdUser = $claims['preferred_username']; + if (($this->config->loginHandler !== null) && $this->config->loginHandler->managesAuthentication()) { + $this->config->loginHandler->authorize2FaUser($openIdUser); + } + else { + $loginAttribute = $this->getLoginAttributeValue($user); + if ($loginAttribute !== $openIdUser) { + logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim preferred_username: ' . $openIdUser); + return false; + } } $this->verificationFailed = false; return true; From 24d9a2c6628369e46164a55c66c366c3e10f51a9 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 16 Feb 2024 20:45:08 +0100 Subject: [PATCH 5/5] #275 2FA login --- lam/HISTORY | 1 + lam/docs/manual-sources/chapter-selfService.xml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lam/HISTORY b/lam/HISTORY index 3fa5fb212..bb2d9bd45 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -5,6 +5,7 @@ March 2024 8.7 -> Cron job to deactivate inactive accounts based on lastBind overlay data (265) -> Request access: support Windows groups (266) -> Request access: usability improvements (278, 279) + -> Self service: passwordless SSO login supported for Okta and OpenID - Fixed bugs: -> User self registration creates accounts only with SSHA hash (287) diff --git a/lam/docs/manual-sources/chapter-selfService.xml b/lam/docs/manual-sources/chapter-selfService.xml index 87e96bb20..52c32de2c 100644 --- a/lam/docs/manual-sources/chapter-selfService.xml +++ b/lam/docs/manual-sources/chapter-selfService.xml @@ -202,7 +202,10 @@ server is responsible to authenticate your users. LAM will use the given user name + password for the LDAP login. To setup HTTP authentication in Apache please see this link. + url="http://httpd.apache.org/docs/2.2/howto/auth.html">link. + If you use Okta or OpenID for 2FA then you can also select to + trust the 2FA provider. In this case the user does not need to + enter any password in LAM itself (SSO).