mirror of
https://github.com/LDAPAccountManager/lam.git
synced 2025-10-03 01:39:33 +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)
|
||||
-> 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)
|
||||
|
||||
|
|
|
@ -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 <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>
|
||||
|
|
|
@ -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;
|
||||
|
@ -771,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;
|
||||
|
@ -867,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;
|
||||
|
@ -1250,6 +1268,7 @@ class TwoFactorProviderService {
|
|||
$tfConfig->twoFactorRememberDeviceDuration = $profile->twoFactorRememberDeviceDuration;
|
||||
$tfConfig->twoFactorRememberDevicePassword = $profile->twoFactorRememberDevicePassword;
|
||||
}
|
||||
$tfConfig->loginHandler = $profile->getLoginHandler();
|
||||
return $tfConfig;
|
||||
}
|
||||
|
||||
|
@ -1362,4 +1381,9 @@ class TwoFactorConfiguration {
|
|||
*/
|
||||
public string $twoFactorRememberDevicePassword = '';
|
||||
|
||||
/**
|
||||
* @var SelfServiceLoginHandler|null login handler
|
||||
*/
|
||||
public ?SelfServiceLoginHandler $loginHandler = null;
|
||||
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
@ -1020,6 +1021,30 @@ 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;
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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