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).
diff --git a/lam/lib/2factor.inc b/lam/lib/2factor.inc
index 89a04b0ed..652eccd4a 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;
@@ -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;
+
}
diff --git a/lam/lib/selfService.inc b/lam/lib/selfService.inc
index c6568155f..8a794e992 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),
};
}
@@ -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));
+ }
+
}