mirror of
https://github.com/LDAPAccountManager/lam.git
synced 2025-10-03 09:49:16 +02:00
Merge pull request #299 from LDAPAccountManager/feature/275_2fa_login
Feature/275 2fa login
This commit is contained in:
commit
e74eb5414e
4 changed files with 222 additions and 15 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,9 +596,11 @@ 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) {
|
||||||
$loginAttribute = $this->getLoginAttributeValue($userDn);
|
if (($this->config->loginHandler === null) || !$this->config->loginHandler->managesAuthentication()) {
|
||||||
if (empty($loginAttribute)) {
|
$loginAttribute = $this->getLoginAttributeValue($userDn);
|
||||||
throw new LAMException('Unable to read login attribute from ' . $userDn);
|
if (empty($loginAttribute)) {
|
||||||
|
throw new LAMException('Unable to read login attribute from ' . $userDn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($this->verificationFailed) {
|
if ($this->verificationFailed) {
|
||||||
return;
|
return;
|
||||||
|
@ -656,10 +659,16 @@ 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));
|
||||||
$loginAttribute = $this->getLoginAttributeValue($user);
|
$oktaUser = $claims['sub'];
|
||||||
if ($loginAttribute !== $claims['sub']) {
|
if (($this->config->loginHandler !== null) && $this->config->loginHandler->managesAuthentication()) {
|
||||||
logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim sub: ' . $claims['sub']);
|
$this->config->loginHandler->authorize2FaUser($oktaUser);
|
||||||
return false;
|
}
|
||||||
|
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;
|
$this->verificationFailed = false;
|
||||||
return true;
|
return true;
|
||||||
|
@ -771,9 +780,12 @@ 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 = $this->getLoginAttributeValue($userDn);
|
$loginAttribute = '';
|
||||||
if (empty($loginAttribute)) {
|
if (($this->config->loginHandler === null) || !$this->config->loginHandler->managesAuthentication()) {
|
||||||
throw new LAMException('Unable to read login attribute from ' . $userDn);
|
$loginAttribute = $this->getLoginAttributeValue($userDn);
|
||||||
|
if (empty($loginAttribute)) {
|
||||||
|
throw new LAMException('Unable to read login attribute from ' . $userDn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($this->verificationFailed) {
|
if ($this->verificationFailed) {
|
||||||
return;
|
return;
|
||||||
|
@ -867,10 +879,16 @@ 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));
|
||||||
$loginAttribute = $this->getLoginAttributeValue($user);
|
$openIdUser = $claims['preferred_username'];
|
||||||
if ($loginAttribute !== $claims['preferred_username']) {
|
if (($this->config->loginHandler !== null) && $this->config->loginHandler->managesAuthentication()) {
|
||||||
logNewMessage(LOG_ERR, 'User name ' . $loginAttribute . ' does not match claim preferred_username: ' . $claims['preferred_username']);
|
$this->config->loginHandler->authorize2FaUser($openIdUser);
|
||||||
return false;
|
}
|
||||||
|
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;
|
$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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue