KEYCLOAK-15511 OTP registration during login with LDAP read-only

When LDAP user federation is configured in read-only mode, it is not
possible to set required actions for users from LDAP.
Keycloak credential model allows for registering OTP devices when LDAP
ist configured with "Import Users" flag enabled. Registering OTP devices
needs to be done via the account management console and works as
expecetd. However, it fails, if a user has to register aN OTP device
during login (i.e. within the authentication flow), because the OTP Form
Authenticator tries to enforce OTP registration via setting the
corresponding required action for the user. That fails, because the user
is read-only.
To work around this, the required action is set on the authentication
session instead.
This commit is contained in:
Sven-Torben Janus 2020-09-11 07:27:56 +02:00 committed by Marek Posolda
parent a965025be8
commit 850d3e7fef
4 changed files with 36 additions and 6 deletions

View file

@ -36,6 +36,7 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@ -129,8 +130,9 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
if (!user.getRequiredActions().contains(UserModel.RequiredAction.CONFIGURE_TOTP.name())) {
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP.name());
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
if (!authenticationSession.getRequiredActions().contains(UserModel.RequiredAction.CONFIGURE_TOTP.name())) {
authenticationSession.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
}
}

View file

@ -49,6 +49,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@ -234,8 +235,9 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// ask the user to do required action to register webauthn authenticator
if (!user.getRequiredActions().contains(WebAuthnRegisterFactory.PROVIDER_ID)) {
user.addRequiredAction(WebAuthnRegisterFactory.PROVIDER_ID);
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
if (!authenticationSession.getRequiredActions().contains(WebAuthnRegisterFactory.PROVIDER_ID)) {
authenticationSession.addRequiredAction(WebAuthnRegisterFactory.PROVIDER_ID);
}
}

View file

@ -33,6 +33,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* Authenticator for WebAuthn authentication with passwordless credential. This class is temporary and will be likely
@ -57,8 +58,9 @@ public class WebAuthnPasswordlessAuthenticator extends WebAuthnAuthenticator {
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// ask the user to do required action to register webauthn authenticator
if (!user.getRequiredActions().contains(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)) {
user.addRequiredAction(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
if (!authenticationSession.getRequiredActions().contains(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)) {
authenticationSession.addRequiredAction(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
}
}

View file

@ -23,6 +23,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
@ -329,6 +330,7 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
@Test
public void setupTotpExisting() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
totpPage.assertCurrent();
@ -358,7 +360,29 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().assertEvent();
}
//KEYCLOAK-15511
@Test
public void setupTotpEnforcedBySessionNotForUserInGeneral() {
String username = "test-user@localhost";
String configureTotp = UserModel.RequiredAction.CONFIGURE_TOTP.name();
// Remove required action from the user
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), username);
UserRepresentation userRepresentation = user.toRepresentation();
userRepresentation.getRequiredActions().remove(configureTotp);
user.update(userRepresentation);
// login
loginPage.open();
loginPage.login(username, "password");
// ensure TOTP configuration is enforced for current authentication session
totpPage.assertCurrent();
// ensure TOTP configuration it is not enforced for the user in general
userRepresentation = user.toRepresentation();
assertFalse(userRepresentation.getRequiredActions().contains(configureTotp));
}
@Test
public void setupTotpRegisteredAfterTotpRemoval() {