Merge pull request #299 from LDAPAccountManager/feature/275_2fa_login

Feature/275 2fa login
This commit is contained in:
gruberroland 2024-02-16 20:48:05 +01:00 committed by GitHub
commit e74eb5414e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 222 additions and 15 deletions

View file

@ -5,6 +5,7 @@ March 2024 8.7
-> Cron job to deactivate inactive accounts based on lastBind overlay data (265) -> Cron job to deactivate inactive accounts based on lastBind overlay data (265)
-> Request access: support Windows groups (266) -> Request access: support Windows groups (266)
-> Request access: usability improvements (278, 279) -> Request access: usability improvements (278, 279)
-> Self service: passwordless SSO login supported for Okta and OpenID
- Fixed bugs: - Fixed bugs:
-> User self registration creates accounts only with SSHA hash (287) -> User self registration creates accounts only with SSHA hash (287)

View file

@ -202,7 +202,10 @@
server is responsible to authenticate your users. LAM will use server is responsible to authenticate your users. LAM will use
the given user name + password for the LDAP login. To setup HTTP the given user name + password for the LDAP login. To setup HTTP
authentication in Apache please see this <ulink authentication in Apache please see this <ulink
url="http://httpd.apache.org/docs/2.2/howto/auth.html">link</ulink>.</entry> url="http://httpd.apache.org/docs/2.2/howto/auth.html">link</ulink>.
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).</entry>
</row> </row>
<row> <row>

View file

@ -7,6 +7,7 @@ use Duo\DuoUniversal\DuoException;
use Exception; use Exception;
use \htmlResponsiveRow; use \htmlResponsiveRow;
use \LAM\LOGIN\WEBAUTHN\WebauthnManager; use \LAM\LOGIN\WEBAUTHN\WebauthnManager;
use SelfServiceLoginHandler;
use \selfServiceProfile; use \selfServiceProfile;
use \LAMConfig; use \LAMConfig;
use \htmlScript; use \htmlScript;
@ -595,10 +596,12 @@ class OktaProvider extends BaseProvider {
* @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput() * @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput()
*/ */
public function addCustomInput(&$row, $userDn) { public function addCustomInput(&$row, $userDn) {
if (($this->config->loginHandler === null) || !$this->config->loginHandler->managesAuthentication()) {
$loginAttribute = $this->getLoginAttributeValue($userDn); $loginAttribute = $this->getLoginAttributeValue($userDn);
if (empty($loginAttribute)) { if (empty($loginAttribute)) {
throw new LAMException('Unable to read login attribute from ' . $userDn); throw new LAMException('Unable to read login attribute from ' . $userDn);
} }
}
if ($this->verificationFailed) { if ($this->verificationFailed) {
return; return;
} }
@ -656,11 +659,17 @@ class OktaProvider extends BaseProvider {
try { try {
$claims = json_decode(base64_decode(explode('.', $accessCode)[1]), true); $claims = json_decode(base64_decode(explode('.', $accessCode)[1]), true);
logNewMessage(LOG_DEBUG, 'Okta claims: ' . print_r($claims, true)); logNewMessage(LOG_DEBUG, 'Okta claims: ' . print_r($claims, true));
$oktaUser = $claims['sub'];
if (($this->config->loginHandler !== null) && $this->config->loginHandler->managesAuthentication()) {
$this->config->loginHandler->authorize2FaUser($oktaUser);
}
else {
$loginAttribute = $this->getLoginAttributeValue($user); $loginAttribute = $this->getLoginAttributeValue($user);
if ($loginAttribute !== $claims['sub']) { if ($loginAttribute !== $oktaUser) {
logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim sub: ' . $claims['sub']); logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim sub: ' . $oktaUser);
return false; return false;
} }
}
$this->verificationFailed = false; $this->verificationFailed = false;
return true; return true;
} }
@ -771,10 +780,13 @@ class OpenIdProvider extends BaseProvider {
* @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput() * @see \LAM\LIB\TWO_FACTOR\BaseProvider::addCustomInput()
*/ */
public function addCustomInput(&$row, $userDn) { public function addCustomInput(&$row, $userDn) {
$loginAttribute = '';
if (($this->config->loginHandler === null) || !$this->config->loginHandler->managesAuthentication()) {
$loginAttribute = $this->getLoginAttributeValue($userDn); $loginAttribute = $this->getLoginAttributeValue($userDn);
if (empty($loginAttribute)) { if (empty($loginAttribute)) {
throw new LAMException('Unable to read login attribute from ' . $userDn); throw new LAMException('Unable to read login attribute from ' . $userDn);
} }
}
if ($this->verificationFailed) { if ($this->verificationFailed) {
return; return;
} }
@ -867,11 +879,17 @@ class OpenIdProvider extends BaseProvider {
$tokenSet = $authorizationService->callback($client, $callbackParams, $_GET['redirect_uri']); $tokenSet = $authorizationService->callback($client, $callbackParams, $_GET['redirect_uri']);
$claims = $tokenSet->claims(); $claims = $tokenSet->claims();
logNewMessage(LOG_DEBUG, print_r($claims, true)); logNewMessage(LOG_DEBUG, print_r($claims, true));
$openIdUser = $claims['preferred_username'];
if (($this->config->loginHandler !== null) && $this->config->loginHandler->managesAuthentication()) {
$this->config->loginHandler->authorize2FaUser($openIdUser);
}
else {
$loginAttribute = $this->getLoginAttributeValue($user); $loginAttribute = $this->getLoginAttributeValue($user);
if ($loginAttribute !== $claims['preferred_username']) { if ($loginAttribute !== $openIdUser) {
logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim preferred_username: ' . $claims['preferred_username']); logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim preferred_username: ' . $openIdUser);
return false; return false;
} }
}
$this->verificationFailed = false; $this->verificationFailed = false;
return true; return true;
} }
@ -1250,6 +1268,7 @@ class TwoFactorProviderService {
$tfConfig->twoFactorRememberDeviceDuration = $profile->twoFactorRememberDeviceDuration; $tfConfig->twoFactorRememberDeviceDuration = $profile->twoFactorRememberDeviceDuration;
$tfConfig->twoFactorRememberDevicePassword = $profile->twoFactorRememberDevicePassword; $tfConfig->twoFactorRememberDevicePassword = $profile->twoFactorRememberDevicePassword;
} }
$tfConfig->loginHandler = $profile->getLoginHandler();
return $tfConfig; return $tfConfig;
} }
@ -1362,4 +1381,9 @@ class TwoFactorConfiguration {
*/ */
public string $twoFactorRememberDevicePassword = ''; public string $twoFactorRememberDevicePassword = '';
/**
* @var SelfServiceLoginHandler|null login handler
*/
public ?SelfServiceLoginHandler $loginHandler = null;
} }

View file

@ -925,6 +925,7 @@ class selfServiceProfile {
public function getLoginHandler(): SelfServiceLoginHandler { public function getLoginHandler(): SelfServiceLoginHandler {
return match ($this->loginHandler) { return match ($this->loginHandler) {
SelfServiceHttpAuthLoginHandler::ID => new SelfServiceHttpAuthLoginHandler($this), SelfServiceHttpAuthLoginHandler::ID => new SelfServiceHttpAuthLoginHandler($this),
SelfService2FaLoginHandler::ID => new SelfService2FaLoginHandler($this),
default => new SelfServiceUserPasswordLoginHandler($this), default => new SelfServiceUserPasswordLoginHandler($this),
}; };
} }
@ -1020,6 +1021,30 @@ interface SelfServiceLoginHandler {
*/ */
function getLoginPassword(): string; 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;
/**
* Authorizes an user that was provided by 2FA provider.
*
* @param string $userName user name
* @throws LAMException error during authentication
*/
function authorize2FaUser(string $userName): void;
} }
/** /**
@ -1081,6 +1106,27 @@ class SelfServiceUserPasswordLoginHandler implements SelfServiceLoginHandler {
return $_POST['password']; return $_POST['password'];
} }
/**
* @inheritDoc
*/
function managesAuthentication(): bool {
return false;
}
/**
* @inheritDoc
*/
function isAuthenticationSuccessful(): bool {
throw new LAMException('Not implemented');
}
/**
* @inheritDoc
*/
function authorize2FaUser(string $userName): void {
// no action
}
} }
/** /**
@ -1142,4 +1188,137 @@ class SelfServiceHttpAuthLoginHandler implements SelfServiceLoginHandler {
return $_SERVER['PHP_AUTH_PW']; return $_SERVER['PHP_AUTH_PW'];
} }
/**
* @inheritDoc
*/
function managesAuthentication(): bool {
return false;
}
/**
* @inheritDoc
*/
function isAuthenticationSuccessful(): bool {
throw new LAMException('Not implemented');
}
/**
* @inheritDoc
*/
function authorize2FaUser(string $userName): void {
// no action
}
}
/**
* 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 '';
}
/**
* @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;
}
/**
* @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));
}
} }