Replace 'Store LoA in User Session' with 'Max Age'. Refactoring of step-up authentications related to that.

Closes #10205
This commit is contained in:
mposolda 2022-02-23 17:25:27 +01:00 committed by Marek Posolda
parent 2bae2d2167
commit 93bba8e338
16 changed files with 702 additions and 168 deletions

View file

@ -131,7 +131,13 @@ public final class Constants {
public static final String CLIENT_POLICIES = "client-policies.policies"; public static final String CLIENT_POLICIES = "client-policies.policies";
// Authentication session note, which contains loa of current authentication in progress
public static final String LEVEL_OF_AUTHENTICATION = "level-of-authentication"; public static final String LEVEL_OF_AUTHENTICATION = "level-of-authentication";
// Authentication session (and user session) note, which contains map with authenticated levels and the times of their authentications,
// so it is possible to check when particular level expires and needs to be re-authenticated
public static final String LOA_MAP = "loa-map";
public static final String REQUESTED_LEVEL_OF_AUTHENTICATION = "requested-level-of-authentication"; public static final String REQUESTED_LEVEL_OF_AUTHENTICATION = "requested-level-of-authentication";
public static final String FORCE_LEVEL_OF_AUTHENTICATION = "force-level-of-authentication"; public static final String FORCE_LEVEL_OF_AUTHENTICATION = "force-level-of-authentication";
public static final String ACR_LOA_MAP = "acr.loa.map"; public static final String ACR_LOA_MAP = "acr.loa.map";

View file

@ -19,11 +19,7 @@ package org.keycloak.authentication;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
@ -32,10 +28,9 @@ import org.keycloak.utils.StringUtil;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream;
import static org.keycloak.services.managers.AuthenticationManager.FORCED_REAUTHENTICATION;
import static org.keycloak.services.managers.AuthenticationManager.SSO_AUTH; import static org.keycloak.services.managers.AuthenticationManager.SSO_AUTH;
public class AuthenticatorUtil { public class AuthenticatorUtil {
@ -50,28 +45,8 @@ public class AuthenticatorUtil {
return "true".equals(authSession.getAuthNote(SSO_AUTH)); return "true".equals(authSession.getAuthNote(SSO_AUTH));
} }
public static boolean isLevelOfAuthenticationForced(AuthenticationSessionModel authSession) { public static boolean isForcedReauthentication(AuthenticationSessionModel authSession) {
return Boolean.parseBoolean(authSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION)); return "true".equals(authSession.getAuthNote(FORCED_REAUTHENTICATION));
}
public static int getRequestedLevelOfAuthentication(AuthenticationSessionModel authSession) {
String requiredLoa = authSession.getClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION);
return requiredLoa == null ? Constants.NO_LOA : Integer.parseInt(requiredLoa);
}
public static int getCurrentLevelOfAuthentication(AuthenticationSessionModel authSession) {
String authSessionLoaNote = authSession.getAuthNote(Constants.LEVEL_OF_AUTHENTICATION);
return authSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(authSessionLoaNote);
}
public static boolean isLevelOfAuthenticationSatisfied(AuthenticationSessionModel authSession) {
return AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession)
<= AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession);
}
public static int getCurrentLevelOfAuthentication(AuthenticatedClientSessionModel clientSession) {
String clientSessionLoaNote = clientSession.getNote(Constants.LEVEL_OF_AUTHENTICATION);
return clientSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(clientSessionLoaNote);
} }
/** /**
@ -135,34 +110,4 @@ public class AuthenticatorUtil {
return executions; return executions;
} }
/**
* @param realm
* @return All LoA numbers configured in the conditions in the realm browser flow
*/
public static Stream<Integer> getLoAConfiguredInRealmBrowserFlow(RealmModel realm) {
List<AuthenticationExecutionModel> loaConditions = getExecutionsByType(realm, realm.getBrowserFlow().getId(), ConditionalLoaAuthenticatorFactory.PROVIDER_ID);
if (loaConditions.isEmpty()) {
// Default values used when step-up conditions not used in the browser authentication flow.
// This is used for backwards compatibility and in case when step-up is not configured in the authentication flow (returning 1 in case of "normal" authentication, 0 for SSO authentication)
return Stream.of(Constants.MINIMUM_LOA, 1);
} else {
Stream<Integer> configuredLoas = loaConditions.stream()
.map(authExecution -> realm.getAuthenticatorConfigById(authExecution.getAuthenticatorConfig()))
.filter(Objects::nonNull)
.map(authConfig -> {
String levelAsStr = authConfig.getConfig().get(ConditionalLoaAuthenticator.LEVEL);
try {
// Check it can be cast to number
return Integer.parseInt(levelAsStr);
} catch (NullPointerException | NumberFormatException e) {
logger.warnf("Invalid level '%s' configured for the configuration of LoA condition with alias '%s'. Level should be number.", levelAsStr, authConfig.getAlias());
return null;
}
})
.filter(Objects::nonNull);
// Add 0 as a level used for SSO cookie
return Stream.concat(Stream.of(Constants.MINIMUM_LOA), configuredLoas);
}
}
} }

View file

@ -19,7 +19,7 @@ package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.authenticators.util.AcrStore;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -49,25 +49,31 @@ public class CookieAuthenticator implements Authenticator {
} else { } else {
AuthenticationSessionModel authSession = context.getAuthenticationSession(); AuthenticationSessionModel authSession = context.getAuthenticationSession();
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol()); LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol());
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, authResult.getSession().getNote(Constants.LEVEL_OF_AUTHENTICATION)); authSession.setAuthNote(Constants.LOA_MAP, authResult.getSession().getNote(Constants.LOA_MAP));
context.setUser(authResult.getUser()); context.setUser(authResult.getUser());
AcrStore acrStore = new AcrStore(authSession);
// Cookie re-authentication is skipped if re-authentication is required // Cookie re-authentication is skipped if re-authentication is required
if (protocol.requireReauthentication(authResult.getSession(), authSession)) { if (protocol.requireReauthentication(authResult.getSession(), authSession)) {
// Full re-authentication, so we start with no loa // Full re-authentication, so we start with no loa
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA)); acrStore.setLevelAuthenticatedToCurrentRequest(Constants.NO_LOA);
authSession.setAuthNote(AuthenticationManager.FORCED_REAUTHENTICATION, "true");
context.setForwardedInfoMessage(Messages.REAUTHENTICATE); context.setForwardedInfoMessage(Messages.REAUTHENTICATE);
context.attempted(); context.attempted();
} else if (!AuthenticatorUtil.isLevelOfAuthenticationSatisfied(authSession)) {
// Step-up authentication, we keep the loa from the existing user session.
// The cookie alone is not enough and other authentications must follow.
context.attempted();
} else { } else {
// Cookie only authentication, no loa is returned int previouslyAuthenticatedLevel = acrStore.getHighestAuthenticatedLevelFromPreviousAuthentication();
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA)); if (acrStore.getRequestedLevelOfAuthentication() > previouslyAuthenticatedLevel) {
authSession.setAuthNote(AuthenticationManager.SSO_AUTH, "true"); // Step-up authentication, we keep the loa from the existing user session.
context.attachUserSession(authResult.getSession()); // The cookie alone is not enough and other authentications must follow.
context.success(); acrStore.setLevelAuthenticatedToCurrentRequest(previouslyAuthenticatedLevel);
context.attempted();
} else {
// Cookie only authentication
acrStore.setLevelAuthenticatedToCurrentRequest(previouslyAuthenticatedLevel);
authSession.setAuthNote(AuthenticationManager.SSO_AUTH, "true");
context.attachUserSession(authResult.getSession());
context.success();
}
} }
} }

View file

@ -23,6 +23,8 @@ import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException; import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.authenticators.util.AcrStore;
import org.keycloak.authentication.authenticators.util.LoAUtil;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -32,6 +34,11 @@ import org.keycloak.sessions.AuthenticationSessionModel;
public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, AuthenticationFlowCallback { public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, AuthenticationFlowCallback {
public static final String LEVEL = "loa-condition-level"; public static final String LEVEL = "loa-condition-level";
public static final String MAX_AGE = "loa-max-age";
public static final int DEFAULT_MAX_AGE = 36000; // 10 days
// Only for backwards compatibility with Keycloak 17
@Deprecated
public static final String STORE_IN_USER_SESSION = "loa-store-in-user-session"; public static final String STORE_IN_USER_SESSION = "loa-store-in-user-session";
private static final Logger logger = Logger.getLogger(ConditionalLoaAuthenticator.class); private static final Logger logger = Logger.getLogger(ConditionalLoaAuthenticator.class);
@ -45,59 +52,77 @@ public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, Au
@Override @Override
public boolean matchCondition(AuthenticationFlowContext context) { public boolean matchCondition(AuthenticationFlowContext context) {
AuthenticationSessionModel authSession = context.getAuthenticationSession(); AuthenticationSessionModel authSession = context.getAuthenticationSession();
int currentLoa = AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession); AcrStore acrStore = new AcrStore(authSession);
int requestedLoa = AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession); int currentAuthenticationLoa = acrStore.getLevelOfAuthenticationFromCurrentAuthentication();
Integer configuredLoa = getConfiguredLoa(context); Integer configuredLoa = getConfiguredLoa(context);
boolean result = (currentLoa < Constants.MINIMUM_LOA && requestedLoa < Constants.MINIMUM_LOA) if (configuredLoa == null) configuredLoa = Constants.MINIMUM_LOA;
|| ((configuredLoa == null || currentLoa < configuredLoa) && currentLoa < requestedLoa); int requestedLoa = acrStore.getRequestedLevelOfAuthentication();
if (currentAuthenticationLoa < Constants.MINIMUM_LOA) {
logger.tracef("Condition '%s' evaluated to true due the user not yet reached any authentication level in this session, configuredLoa: %d, requestedLoa: %d",
context.getAuthenticatorConfig().getAlias(), configuredLoa, requestedLoa);
return true;
} else {
if (requestedLoa < configuredLoa) {
logger.tracef("Condition '%s' evaluated to false due the requestedLoa '%d' smaller than configuredLoa '%d'. CurrentAuthenticationLoa: %d",
context.getAuthenticatorConfig().getAlias(), requestedLoa, configuredLoa, currentAuthenticationLoa);
return false;
}
int maxAge = getMaxAge(context);
boolean previouslyAuthenticated = (acrStore.isLevelAuthenticatedInPreviousAuth(configuredLoa, maxAge));
if (previouslyAuthenticated) {
if (currentAuthenticationLoa < configuredLoa) {
acrStore.setLevelAuthenticatedToCurrentRequest(configuredLoa);
}
}
logger.tracef("Checking condition '%s' : currentAuthenticationLoa: %d, requestedLoa: %d, configuredLoa: %d, evaluation result: %b",
context.getAuthenticatorConfig().getAlias(), currentAuthenticationLoa, requestedLoa, configuredLoa, !previouslyAuthenticated);
logger.tracef("Checking condition '%s' : currentLoa: %d, requestedLoa: %d, configuredLoa: %d, evaluation result: %b", return !previouslyAuthenticated;
context.getAuthenticatorConfig().getAlias(), currentLoa, requestedLoa, configuredLoa, result); }
return result;
} }
@Override @Override
public void onParentFlowSuccess(AuthenticationFlowContext context) { public void onParentFlowSuccess(AuthenticationFlowContext context) {
AuthenticationSessionModel authSession = context.getAuthenticationSession(); AuthenticationSessionModel authSession = context.getAuthenticationSession();
AcrStore acrStore = new AcrStore(authSession);
Integer newLoa = getConfiguredLoa(context); Integer newLoa = getConfiguredLoa(context);
if (newLoa == null) { if (newLoa == null) {
return; return;
} }
logger.tracef("Updating LoA to '%d' when authenticating session '%s'", newLoa, authSession.getParentSession().getId()); int maxAge = getMaxAge(context);
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(newLoa)); if (maxAge == 0) {
if (isStoreInUserSession(context)) { logger.tracef("Skip updating authenticated level '%d' in condition '%s' for future authentications due max-age set to 0", newLoa, context.getAuthenticatorConfig().getAlias());
authSession.setUserSessionNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(newLoa)); acrStore.setLevelAuthenticatedToCurrentRequest(newLoa);
} else {
logger.tracef("Updating LoA to '%d' in the condition '%s' when authenticating session '%s'. Max age is %d.",
newLoa, context.getAuthenticatorConfig().getAlias(), authSession.getParentSession().getId(), maxAge);
acrStore.setLevelAuthenticated(newLoa);
} }
} }
@Override @Override
public void onTopFlowSuccess() { public void onTopFlowSuccess() {
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession(); AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
AcrStore acrStore = new AcrStore(authSession);
if (AuthenticatorUtil.isLevelOfAuthenticationForced(authSession) && !AuthenticatorUtil.isLevelOfAuthenticationSatisfied(authSession) && !AuthenticatorUtil.isSSOAuthentication(authSession)) { logger.tracef("Finished authentication at level %d when authenticating authSession '%s'.", acrStore.getLevelOfAuthenticationFromCurrentAuthentication(), authSession.getParentSession().getId());
if (acrStore.isLevelOfAuthenticationForced() && !acrStore.isLevelOfAuthenticationSatisfiedFromCurrentAuthentication()) {
String details = String.format("Forced level of authentication did not meet the requirements. Requested level: %d, Fulfilled level: %d", String details = String.format("Forced level of authentication did not meet the requirements. Requested level: %d, Fulfilled level: %d",
AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession), AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession)); acrStore.getRequestedLevelOfAuthentication(), acrStore.getLevelOfAuthenticationFromCurrentAuthentication());
throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, details, Messages.ACR_NOT_FULFILLED); throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, details, Messages.ACR_NOT_FULFILLED);
} }
logger.tracef("Updating authenticated levels in authSession '%s' to user session note for future authentications: %s", authSession.getParentSession().getId(), authSession.getAuthNote(Constants.LOA_MAP));
authSession.setUserSessionNote(Constants.LOA_MAP, authSession.getAuthNote(Constants.LOA_MAP));
} }
private Integer getConfiguredLoa(AuthenticationFlowContext context) { private Integer getConfiguredLoa(AuthenticationFlowContext context) {
try { return LoAUtil.getLevelFromLoaConditionConfiguration(context.getAuthenticatorConfig());
return Integer.parseInt(context.getAuthenticatorConfig().getConfig().get(LEVEL));
} catch (NullPointerException | NumberFormatException e) {
logger.errorv("Invalid configuration: {0}", LEVEL);
return null;
}
} }
private boolean isStoreInUserSession(AuthenticationFlowContext context) { private int getMaxAge(AuthenticationFlowContext context) {
try { return LoAUtil.getMaxAgeFromLoaConditionConfiguration(context.getAuthenticatorConfig());
return Boolean.parseBoolean(context.getAuthenticatorConfig().getConfig().get(STORE_IN_USER_SESSION));
} catch (NullPointerException | NumberFormatException e) {
logger.errorv("Invalid configuration: {0}", STORE_IN_USER_SESSION);
return false;
}
} }
@Override @Override

View file

@ -43,11 +43,11 @@ public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthentica
.type(ProviderConfigProperty.STRING_TYPE) .type(ProviderConfigProperty.STRING_TYPE)
.add() .add()
.property() .property()
.name(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION) .name(ConditionalLoaAuthenticator.MAX_AGE)
.label(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION) .label(ConditionalLoaAuthenticator.MAX_AGE)
.helpText(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION + ".tooltip") .helpText(ConditionalLoaAuthenticator.MAX_AGE + ".tooltip")
.type(ProviderConfigProperty.BOOLEAN_TYPE) .type(ProviderConfigProperty.STRING_TYPE)
.defaultValue("true") .defaultValue(ConditionalLoaAuthenticator.DEFAULT_MAX_AGE) // 10 hours
.add() .add()
.build(); .build();

View file

@ -0,0 +1,201 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.authentication.authenticators.util;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import com.fasterxml.jackson.core.type.TypeReference;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
/**
* CRUD data in the authentication session, which are related to step-up authentication
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class AcrStore {
private static final Logger logger = Logger.getLogger(AcrStore.class);
private final AuthenticationSessionModel authSession;
public AcrStore(AuthenticationSessionModel authSession) {
this.authSession = authSession;
}
public boolean isLevelOfAuthenticationForced() {
return Boolean.parseBoolean(authSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION));
}
public int getRequestedLevelOfAuthentication() {
String requiredLoa = authSession.getClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION);
return requiredLoa == null ? Constants.NO_LOA : Integer.parseInt(requiredLoa);
}
public boolean isLevelOfAuthenticationSatisfiedFromCurrentAuthentication() {
return getRequestedLevelOfAuthentication()
<= getAuthenticatedLevelCurrentAuthentication();
}
public static int getCurrentLevelOfAuthentication(AuthenticatedClientSessionModel clientSession) {
String clientSessionLoaNote = clientSession.getNote(Constants.LEVEL_OF_AUTHENTICATION);
return clientSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(clientSessionLoaNote);
}
/**
* @param level level of authentication
* @param maxAge maxAge for which this level is considered valid
* @return True if the particular level was already authenticated before in this userSession and is still valid
*/
public boolean isLevelAuthenticatedInPreviousAuth(int level, int maxAge) {
// In case of re-authentication requested from client (EG. by "prompt=login" or "max_age=0", the LoA from previous authentications are not
// considered. User needs to re-authenticate all requested levels again.
if (AuthenticatorUtil.isForcedReauthentication(authSession)) return false;
Map<Integer, Integer> levels = getCurrentAuthenticatedLevelsMap();
if (levels == null) return false;
Integer levelAuthTime = levels.get(level);
if (levelAuthTime == null) return false;
int currentTime = Time.currentTime();
return levelAuthTime + maxAge >= currentTime;
}
/**
* return level, which was either:
* - directly authenticated in current authentication
* - or was already verified that can be re-used from previous authentication
*
* @return see above
*/
public int getLevelOfAuthenticationFromCurrentAuthentication() {
String authSessionLoaNote = authSession.getAuthNote(Constants.LEVEL_OF_AUTHENTICATION);
return authSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(authSessionLoaNote);
}
/**
* Save authenticated level to authenticationSession (for current authentication) and loa map (for future authentications)
*
* @param level level to save
*/
public void setLevelAuthenticated(int level) {
setLevelAuthenticatedToCurrentRequest(level);
setLevelAuthenticatedToMap(level);
}
/**
* Set level to the current authentication session
*
* @param level, which was authenticated by user
*/
public void setLevelAuthenticatedToCurrentRequest(int level) {
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(level));
}
private void setLevelAuthenticatedToMap(int level) {
Map<Integer, Integer> levels = getCurrentAuthenticatedLevelsMap();
if (levels == null) levels = new HashMap<>();
levels.put(level, Time.currentTime());
saveCurrentAuthenticatedLevelsMap(levels);
}
private int getAuthenticatedLevelCurrentAuthentication() {
String authSessionLoaNote = authSession.getAuthNote(Constants.LEVEL_OF_AUTHENTICATION);
return authSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(authSessionLoaNote);
}
/**
* @return highest authenticated level from previous authentication, which is still valid (not yet expired)
*/
public int getHighestAuthenticatedLevelFromPreviousAuthentication() {
// No map found. User was not yet authenticated in this session
Map<Integer, Integer> levels = getCurrentAuthenticatedLevelsMap();
if (levels == null || levels.isEmpty()) return Constants.NO_LOA;
// Map was already saved, so it is SSO authentication at minimum. Using "0" level as the minimum level in this case
int maxLevel = Constants.MINIMUM_LOA;
int currentTime = Time.currentTime();
Map<Integer, Integer> configuredMaxAges = LoAUtil.getLoaMaxAgesConfiguredInRealmBrowserFlow(authSession.getRealm());
levels = new TreeMap<>(levels);
for (Map.Entry<Integer, Integer> entry : levels.entrySet()) {
Integer levelMaxAge = configuredMaxAges.get(entry.getKey());
if (levelMaxAge == null) {
logger.warnf("No condition found for level '%d' in the authentication flow", entry.getKey());
levelMaxAge = 0;
}
int levelAuthTime = entry.getValue();
int levelExpiration = levelAuthTime + levelMaxAge;
if (currentTime <= levelExpiration) {
maxLevel = entry.getKey();
} else {
break;
}
}
logger.tracef("Highest authenticated level from previous authentication of client '%s' in authentication '%s' was: %d",
authSession.getClient().getClientId(), authSession.getParentSession().getId(), maxLevel);
return maxLevel;
}
// Key is level number. Value is level authTime
private Map<Integer, Integer> getCurrentAuthenticatedLevelsMap() {
String loaMap = authSession.getAuthNote(Constants.LOA_MAP);
if (loaMap == null) {
return null;
}
try {
return JsonSerialization.readValue(loaMap, new TypeReference<Map<Integer, Integer>>() {});
} catch (IOException e) {
logger.warnf("Invalid format of the LoA map. Saved value was: %s", loaMap);
throw new IllegalStateException(e);
}
}
private void saveCurrentAuthenticatedLevelsMap(Map<Integer, Integer> levelInfoMap) {
try {
String note = JsonSerialization.writeValueAsString(levelInfoMap);
authSession.setAuthNote(Constants.LOA_MAP, note);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.authentication.authenticators.util;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LoAUtil {
private static final Logger logger = Logger.getLogger(LoAUtil.class);
/**
* @param clientSession
* @return current level from client session
*/
public static int getCurrentLevelOfAuthentication(AuthenticatedClientSessionModel clientSession) {
String clientSessionLoaNote = clientSession.getNote(Constants.LEVEL_OF_AUTHENTICATION);
return clientSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(clientSessionLoaNote);
}
/**
* @param realm
* @return All LoA numbers configured in the conditions in the realm browser flow
*/
public static Stream<Integer> getLoAConfiguredInRealmBrowserFlow(RealmModel realm) {
Map<Integer, Integer> loaMaxAges = getLoaMaxAgesConfiguredInRealmBrowserFlow(realm);
if (loaMaxAges.isEmpty()) {
// Default values used when step-up conditions not used in the browser authentication flow.
// This is used for backwards compatibility and in case when step-up is not configured in the authentication flow (returning 1 in case of "normal" authentication, 0 for SSO authentication)
return Stream.of(Constants.MINIMUM_LOA, 1);
} else {
// Add 0 as a level used for SSO cookie
return Stream.concat(Stream.of(Constants.MINIMUM_LOA), loaMaxAges.keySet().stream());
}
}
/**
* @param realm
* @return All LoA numbers configured in the conditions in the realm browser flow. Key is level, Vaue is maxAge for particular level
*/
public static Map<Integer, Integer> getLoaMaxAgesConfiguredInRealmBrowserFlow(RealmModel realm) {
List<AuthenticationExecutionModel> loaConditions = AuthenticatorUtil.getExecutionsByType(realm, realm.getBrowserFlow().getId(), ConditionalLoaAuthenticatorFactory.PROVIDER_ID);
if (loaConditions.isEmpty()) {
// Default values used when step-up conditions not used in the browser authentication flow.
// This is used for backwards compatibility and in case when step-up is not configured in the authentication flow (returning 1 in case of "normal" authentication, 0 for SSO authentication)
return Collections.emptyMap();
} else {
Map<Integer, Integer> loas = loaConditions.stream()
.map(authExecution -> realm.getAuthenticatorConfigById(authExecution.getAuthenticatorConfig()))
.filter(Objects::nonNull)
.filter(authConfig -> getLevelFromLoaConditionConfiguration(authConfig) != null)
.collect(Collectors.toMap(LoAUtil::getLevelFromLoaConditionConfiguration, LoAUtil::getMaxAgeFromLoaConditionConfiguration));
return loas;
}
}
public static Integer getLevelFromLoaConditionConfiguration(AuthenticatorConfigModel loaConditionConfig) {
String levelAsStr = loaConditionConfig.getConfig().get(ConditionalLoaAuthenticator.LEVEL);
try {
// Check it can be cast to number
return Integer.parseInt(levelAsStr);
} catch (NullPointerException | NumberFormatException e) {
logger.warnf("Invalid level '%s' configured for the configuration of LoA condition with alias '%s'. Level should be number.", levelAsStr, loaConditionConfig.getAlias());
return null;
}
}
public static int getMaxAgeFromLoaConditionConfiguration(AuthenticatorConfigModel loaConditionConfig) {
try {
return Integer.parseInt(loaConditionConfig.getConfig().get(ConditionalLoaAuthenticator.MAX_AGE));
} catch (NullPointerException | NumberFormatException e) {
// Backwards compatibility with Keycloak 17
String storeLoaInUserSession = loaConditionConfig.getConfig().get(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION);
if (storeLoaInUserSession != null) {
int maxAge = Boolean.parseBoolean(storeLoaInUserSession) ? ConditionalLoaAuthenticator.DEFAULT_MAX_AGE : 0;
logger.warnf("Max age not configured for condition '%s' in the authentication flow. Fallback to %d based on the configuration option %s from previous version",
loaConditionConfig.getAlias(), maxAge, ConditionalLoaAuthenticator.STORE_IN_USER_SESSION);
return maxAge;
}
logger.errorf("Invalid max age configured for condition '%s'. Fallback to 0", loaConditionConfig.getAlias());
return 0;
}
}
}

View file

@ -19,9 +19,9 @@ package org.keycloak.protocol.oidc;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.ClientAuthenticator; import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.authentication.authenticators.util.LoAUtil;
import org.keycloak.crypto.CekManagementProvider; import org.keycloak.crypto.CekManagementProvider;
import org.keycloak.crypto.ClientSignatureVerifierProvider; import org.keycloak.crypto.ClientSignatureVerifierProvider;
import org.keycloak.crypto.ContentEncryptionProvider; import org.keycloak.crypto.ContentEncryptionProvider;
@ -29,7 +29,6 @@ import org.keycloak.crypto.SignatureProvider;
import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.CibaConfig; import org.keycloak.models.CibaConfig;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
@ -63,7 +62,6 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -264,7 +262,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
List<String> result = new ArrayList<>(realmAcrLoaMap.keySet()); List<String> result = new ArrayList<>(realmAcrLoaMap.keySet());
// Add LoA levels configured in authentication flow in addition to the realm values // Add LoA levels configured in authentication flow in addition to the realm values
result.addAll(AuthenticatorUtil.getLoAConfiguredInRealmBrowserFlow(realm) result.addAll(LoAUtil.getLoAConfiguredInRealmBrowserFlow(realm)
.map(String::valueOf) .map(String::valueOf)
.collect(Collectors.toList())); .collect(Collectors.toList()));
return result; return result;

View file

@ -24,7 +24,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.TokenCategory; import org.keycloak.TokenCategory;
import org.keycloak.TokenVerifier; import org.keycloak.TokenVerifier;
import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.authenticators.util.AcrStore;
import org.keycloak.authentication.authenticators.util.LoAUtil;
import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ClusterProvider;
@ -565,7 +566,7 @@ public class TokenManager {
userSession.setNote(entry.getKey(), entry.getValue()); userSession.setNote(entry.getKey(), entry.getValue());
} }
clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession))); clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(new AcrStore(authSession).getLevelOfAuthenticationFromCurrentAuthentication()));
clientSession.setTimestamp(Time.currentTime()); clientSession.setTimestamp(Time.currentTime());
// Remove authentication session now // Remove authentication session now
@ -901,7 +902,7 @@ public class TokenManager {
} }
private String getAcr(AuthenticatedClientSessionModel clientSession) { private String getAcr(AuthenticatedClientSessionModel clientSession) {
int loa = AuthenticatorUtil.getCurrentLevelOfAuthentication(clientSession); int loa = LoAUtil.getCurrentLevelOfAuthentication(clientSession);
if (loa < Constants.MINIMUM_LOA) { if (loa < Constants.MINIMUM_LOA) {
loa = AuthenticationManager.isSSOAuthentication(clientSession) ? 0 : 1; loa = AuthenticationManager.isSSOAuthentication(clientSession) ? 0 : 1;
} }
@ -920,6 +921,8 @@ public class TokenManager {
} }
} }
} }
logger.tracef("Level sent in the token to client %s: %s. Original loa from the authentication: %d", clientSession.getClient().getClientId(), acr, loa);
return acr; return acr;
} }

View file

@ -127,7 +127,7 @@ public class AcrUtils {
public static String mapLoaToAcr(int loa, Map<String, Integer> acrLoaMap, Collection<String> acrValues) { public static String mapLoaToAcr(int loa, Map<String, Integer> acrLoaMap, Collection<String> acrValues) {
String acr = null; String acr = null;
if (!acrLoaMap.isEmpty() && !acrValues.isEmpty()) { if (!acrLoaMap.isEmpty() && !acrValues.isEmpty()) {
int maxLoa = 0; int maxLoa = -1;
for (String acrValue : acrValues) { for (String acrValue : acrValues) {
Integer mappedLoa = acrLoaMap.get(acrValue); Integer mappedLoa = acrLoaMap.get(acrValue);
// if there is no mapping for the acrValue, it may be an integer itself // if there is no mapping for the acrValue, it may be an integer itself

View file

@ -143,6 +143,9 @@ public class AuthenticationManager {
// clientSession note with flag that clientSession was authenticated through SSO cookie // clientSession note with flag that clientSession was authenticated through SSO cookie
public static final String SSO_AUTH = "SSO_AUTH"; public static final String SSO_AUTH = "SSO_AUTH";
// authSession note with flag that is true if user is forced to re-authenticate by client (EG. in case of OIDC client by sending "prompt=login")
public static final String FORCED_REAUTHENTICATION = "FORCED_REAUTHENTICATION";
protected static final Logger logger = Logger.getLogger(AuthenticationManager.class); protected static final Logger logger = Logger.getLogger(AuthenticationManager.class);
public static final String FORM_USERNAME = "username"; public static final String FORM_USERNAME = "username";

View file

@ -17,6 +17,7 @@
package org.keycloak.validation; package org.keycloak.validation;
import org.keycloak.authentication.AuthenticatorUtil; import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.authenticators.util.LoAUtil;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.protocol.ProtocolMapperConfigException; import org.keycloak.protocol.ProtocolMapperConfigException;
@ -279,7 +280,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
} }
for (String configuredAcr : defaultAcrValues) { for (String configuredAcr : defaultAcrValues) {
if (acrToLoaMap.containsKey(configuredAcr)) continue; if (acrToLoaMap.containsKey(configuredAcr)) continue;
if (!AuthenticatorUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm()) if (!LoAUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm())
.anyMatch(level -> configuredAcr.equals(String.valueOf(level)))) { .anyMatch(level -> configuredAcr.equals(String.valueOf(level)))) {
context.addError("defaultAcrValues", "Default ACR values need to contain values specified in the ACR-To-Loa mapping or number levels from set realm browser flow"); context.addError("defaultAcrValues", "Default ACR values need to contain values specified in the ACR-To-Loa mapping or number levels from set realm browser flow");
} }

View file

@ -166,6 +166,8 @@ public class OAuthClient {
private String maxAge; private String maxAge;
private String prompt;
private String responseType; private String responseType;
private String responseMode; private String responseMode;
@ -255,6 +257,7 @@ public class OAuthClient {
clientSessionState = null; clientSessionState = null;
clientSessionHost = null; clientSessionHost = null;
maxAge = null; maxAge = null;
prompt = null;
responseType = OAuth2Constants.CODE; responseType = OAuth2Constants.CODE;
responseMode = null; responseMode = null;
nonce = null; nonce = null;
@ -1133,6 +1136,9 @@ public class OAuthClient {
if (maxAge != null) { if (maxAge != null) {
parameters.add(new BasicNameValuePair(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge)); parameters.add(new BasicNameValuePair(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge));
} }
if (prompt != null) {
parameters.add(new BasicNameValuePair(OIDCLoginProtocol.PROMPT_PARAM, prompt));
}
if (request != null) { if (request != null) {
parameters.add(new BasicNameValuePair(OIDCLoginProtocol.REQUEST_PARAM, request)); parameters.add(new BasicNameValuePair(OIDCLoginProtocol.REQUEST_PARAM, request));
} }
@ -1418,6 +1424,9 @@ public class OAuthClient {
if (maxAge != null) { if (maxAge != null) {
b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge); b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
} }
if (prompt != null) {
b.queryParam(OIDCLoginProtocol.PROMPT_PARAM, prompt);
}
if (request != null) { if (request != null) {
b.queryParam(OIDCLoginProtocol.REQUEST_PARAM, request); b.queryParam(OIDCLoginProtocol.REQUEST_PARAM, request);
} }
@ -1622,6 +1631,11 @@ public class OAuthClient {
return this; return this;
} }
public OAuthClient prompt(String prompt) {
this.prompt = prompt;
return this;
}
public OAuthClient responseType(String responseType) { public OAuthClient responseType(String responseType) {
this.responseType = responseType; this.responseType = responseType;
return this; return this;

View file

@ -93,7 +93,7 @@ public class AuthenticationFlowCallbackProviderTest extends AbstractTestRealmKey
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID, .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
config -> { config -> {
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1"); config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1");
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "true"); config.getConfig().put(ConditionalLoaAuthenticator.MAX_AGE, String.valueOf(ConditionalLoaAuthenticator.DEFAULT_MAX_AGE));
}) })
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, AllowAccessAuthenticatorFactory.PROVIDER_ID) .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, AllowAccessAuthenticatorFactory.PROVIDER_ID)
) )

View file

@ -32,19 +32,22 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory; import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.models.AuthenticationExecutionModel.Requirement; import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.ClaimsRepresentation; import org.keycloak.representations.ClaimsRepresentation;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
@ -52,11 +55,14 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory; import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.PasswordPage; import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.PushTheButtonPage; import org.keycloak.testsuite.pages.PushTheButtonPage;
import org.keycloak.testsuite.util.FlowUtil; import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmRepUtil;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
@ -74,10 +80,12 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
public AssertEvents events = new AssertEvents(this); public AssertEvents events = new AssertEvents(this);
@Page @Page
protected LoginUsernameOnlyPage loginUsernameOnlyPage; protected LoginPage loginPage;
@Page @Page
protected PasswordPage passwordPage; protected LoginTotpPage loginTotpPage;
private TimeBasedOTP totp = new TimeBasedOTP();
@Page @Page
protected PushTheButtonPage pushTheButtonPage; protected PushTheButtonPage pushTheButtonPage;
@ -89,6 +97,10 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
public void configureTestRealm(RealmRepresentation testRealm) { public void configureTestRealm(RealmRepresentation testRealm) {
try { try {
findTestApp(testRealm).setAttributes(Collections.singletonMap(Constants.ACR_LOA_MAP, getAcrToLoaMappingForClient())); findTestApp(testRealm).setAttributes(Collections.singletonMap(Constants.ACR_LOA_MAP, getAcrToLoaMappingForClient()));
UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost");
UserBuilder.edit(user)
.totpSecret("totpSecret")
.otpEnabled();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -108,6 +120,10 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
} }
public static void configureStepUpFlow(KeycloakTestingClient testingClient) { public static void configureStepUpFlow(KeycloakTestingClient testingClient) {
configureStepUpFlow(testingClient, ConditionalLoaAuthenticator.DEFAULT_MAX_AGE, 0, 0);
}
private static void configureStepUpFlow(KeycloakTestingClient testingClient, int maxAge1, int maxAge2, int maxAge3) {
final String newFlowAlias = "browser - Level of Authentication FLow"; final String newFlowAlias = "browser - Level of Authentication FLow";
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias)); testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server(TEST_REALM_NAME) testingClient.server(TEST_REALM_NAME)
@ -117,26 +133,32 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID, subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
config -> { config -> {
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1"); config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1");
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "true"); config.getConfig().put(ConditionalLoaAuthenticator.MAX_AGE, String.valueOf(maxAge1));
}); });
// username input for level 1 // username input for level 1
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID); subFlow.addAuthenticatorExecution(Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID);
}) })
// level 2 authentication // level 2 authentication
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> { .addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID, subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
config -> config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "2")); config -> {
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "2");
config.getConfig().put(ConditionalLoaAuthenticator.MAX_AGE, String.valueOf(maxAge2));
});
// password required for level 2 // password required for level 2
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PasswordFormFactory.PROVIDER_ID); subFlow.addAuthenticatorExecution(Requirement.REQUIRED, OTPFormAuthenticatorFactory.PROVIDER_ID);
}) })
// level 3 authentication // level 3 authentication
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> { .addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID, subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
config -> config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "3")); config -> {
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "3");
config.getConfig().put(ConditionalLoaAuthenticator.MAX_AGE, String.valueOf(maxAge3));
});
// simply push button for level 3 // simply push button for level 3
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PushButtonAuthenticatorFactory.PROVIDER_ID); subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PushButtonAuthenticatorFactory.PROVIDER_ID);
@ -145,6 +167,11 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
).defineAsBrowserFlow()); ).defineAsBrowserFlow());
} }
private void reconfigureStepUpFlow(int maxAge1, int maxAge2, int maxAge3) {
BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow");
configureStepUpFlow(testingClient, maxAge1, maxAge2, maxAge3);
}
@After @After
public void after() { public void after() {
BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow"); BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow");
@ -154,7 +181,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
public void loginWithoutAcr() { public void loginWithoutAcr() {
oauth.openLoginForm(); oauth.openLoginForm();
// Authentication without specific LOA results in level 1 authentication // Authentication without specific LOA results in level 1 authentication
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
} }
@ -162,7 +189,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
public void loginWithAcr1() { public void loginWithAcr1() {
// username input for level 1 // username input for level 1
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
} }
@ -171,8 +198,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
public void loginWithAcr2() { public void loginWithAcr2() {
// username and password input for level 2 // username and password input for level 2
openLoginFormWithAcrClaim(true, "gold"); openLoginFormWithAcrClaim(true, "gold");
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("gold"); assertLoggedInWithAcr("gold");
} }
@ -180,8 +207,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
public void loginWithAcr3() { public void loginWithAcr3() {
// username, password input and finally push button for level 3 // username, password input and finally push button for level 3
openLoginFormWithAcrClaim(true, "3"); openLoginFormWithAcrClaim(true, "3");
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
authenticateWithButton(); authenticateWithButton();
// ACR 3 is returned because it was requested, although there is no mapping for it // ACR 3 is returned because it was requested, although there is no mapping for it
assertLoggedInWithAcr("3"); assertLoggedInWithAcr("3");
@ -191,15 +218,15 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
public void stepupAuthentication() { public void stepupAuthentication() {
// logging in to level 1 // logging in to level 1
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
// doing step-up authentication to level 2 // doing step-up authentication to level 2
openLoginFormWithAcrClaim(true, "gold"); openLoginFormWithAcrClaim(true, "gold");
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("gold"); assertLoggedInWithAcr("gold");
// step-up to level 3 needs password authentication because level 2 is not stored in user session // step-up to level 3 needs password authentication because level 2 is not stored in user session
openLoginFormWithAcrClaim(true, "3"); openLoginFormWithAcrClaim(true, "3");
authenticateWithPassword(); authenticateWithTotp();
authenticateWithButton(); authenticateWithButton();
assertLoggedInWithAcr("3"); assertLoggedInWithAcr("3");
} }
@ -207,7 +234,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void stepupToUnknownEssentialAcrFails() { public void stepupToUnknownEssentialAcrFails() {
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
// step-up to unknown acr // step-up to unknown acr
openLoginFormWithAcrClaim(true, "uranium"); openLoginFormWithAcrClaim(true, "uranium");
@ -217,36 +244,36 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void reauthenticationWithNoAcr() { public void reauthenticationWithNoAcr() {
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
oauth.claims(null); oauth.claims(null);
oauth.openLoginForm(); oauth.openLoginForm();
assertLoggedInWithAcr("0"); assertLoggedInWithAcr("silver"); // Return silver without need to re-authenticate due maxAge for "silver" condition did not timed-out yet
} }
@Test @Test
public void reauthenticationWithReachedAcr() { public void reauthenticationWithReachedAcr() {
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
assertLoggedInWithAcr("0"); assertLoggedInWithAcr("silver"); // Return previous level due maxAge for "silver" condition did not timed-out yet
} }
@Test @Test
public void reauthenticationWithOptionalUnknownAcr() { public void reauthenticationWithOptionalUnknownAcr() {
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
openLoginFormWithAcrClaim(false, "iron"); openLoginFormWithAcrClaim(false, "iron");
assertLoggedInWithAcr("0"); assertLoggedInWithAcr("silver"); // Return silver without need to re-authenticate due maxAge for "silver" condition did not timed-out yet
} }
@Test @Test
public void essentialClaimNotReachedFails() { public void essentialClaimNotReachedFails() {
openLoginFormWithAcrClaim(true, "4"); openLoginFormWithAcrClaim(true, "4");
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
authenticateWithButton(); authenticateWithButton();
assertErrorPage("Authentication requirements not fulfilled"); assertErrorPage("Authentication requirements not fulfilled");
} }
@ -254,8 +281,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void optionalClaimNotReachedSucceeds() { public void optionalClaimNotReachedSucceeds() {
openLoginFormWithAcrClaim(false, "4"); openLoginFormWithAcrClaim(false, "4");
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
authenticateWithButton(); authenticateWithButton();
// the reached loa is 3, but there is no mapping for it, and it was not explicitly // the reached loa is 3, but there is no mapping for it, and it was not explicitly
// requested, so the highest known and reached ACR is returned // requested, so the highest known and reached ACR is returned
@ -271,7 +298,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void optionalUnknownClaimSucceeds() { public void optionalUnknownClaimSucceeds() {
openLoginFormWithAcrClaim(false, "iron"); openLoginFormWithAcrClaim(false, "iron");
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
} }
@ -280,24 +307,24 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
driver.navigate().to(UriBuilder.fromUri(oauth.getLoginFormUrl()) driver.navigate().to(UriBuilder.fromUri(oauth.getLoginFormUrl())
.queryParam("acr_values", "gold 3") .queryParam("acr_values", "gold 3")
.build().toString()); .build().toString());
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("gold"); assertLoggedInWithAcr("gold");
} }
@Test @Test
public void multipleEssentialAcrValues() { public void multipleEssentialAcrValues() {
openLoginFormWithAcrClaim(true, "gold", "3"); openLoginFormWithAcrClaim(true, "gold", "3");
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("gold"); assertLoggedInWithAcr("gold");
} }
@Test @Test
public void multipleOptionalAcrValues() { public void multipleOptionalAcrValues() {
openLoginFormWithAcrClaim(false, "gold", "3"); openLoginFormWithAcrClaim(false, "gold", "3");
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("gold"); assertLoggedInWithAcr("gold");
} }
@ -320,8 +347,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
testClient.update(testClientRep); testClient.update(testClientRep);
openLoginFormWithAcrClaim(true, "realm:gold"); openLoginFormWithAcrClaim(true, "realm:gold");
authenticateWithUsername(); authenticateWithUsernamePassword();
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("realm:gold"); assertLoggedInWithAcr("realm:gold");
// Add "acr-to-loa" back to the client. Client mapping will be used instead of realm mapping // Add "acr-to-loa" back to the client. Client mapping will be used instead of realm mapping
@ -332,7 +359,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
assertErrorPage("Invalid parameter: claims"); assertErrorPage("Invalid parameter: claims");
openLoginFormWithAcrClaim(true, "gold"); openLoginFormWithAcrClaim(true, "gold");
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("gold"); assertLoggedInWithAcr("gold");
// Rollback // Rollback
@ -349,23 +376,23 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
// Should request client to authenticate with silver // Should request client to authenticate with silver
oauth.openLoginForm(); oauth.openLoginForm();
authenticateWithUsername(); authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver"); assertLoggedInWithAcr("silver");
// Re-configure to level gold // Re-configure to level gold
OIDCAdvancedConfigWrapper.fromClientRepresentation(testClientRep).setAttributeMultivalued(Constants.DEFAULT_ACR_VALUES, Arrays.asList("gold")); OIDCAdvancedConfigWrapper.fromClientRepresentation(testClientRep).setAttributeMultivalued(Constants.DEFAULT_ACR_VALUES, Arrays.asList("gold"));
testClient.update(testClientRep); testClient.update(testClientRep);
oauth.openLoginForm(); oauth.openLoginForm();
authenticateWithPassword(); authenticateWithTotp();
assertLoggedInWithAcr("gold"); assertLoggedInWithAcr("gold");
// Value from essential ACR should have preference // Value from essential ACR from claims parameter should have preference over the client default
openLoginFormWithAcrClaim(true, "silver"); openLoginFormWithAcrClaim(true, "silver");
assertLoggedInWithAcr("0"); assertLoggedInWithAcr("silver");
// Value from non-essential ACR should have preference // Value from non-essential ACR from claims parameter should have preference over the client default
openLoginFormWithAcrClaim(false, "silver"); openLoginFormWithAcrClaim(false, "silver");
assertLoggedInWithAcr("0"); assertLoggedInWithAcr("silver");
// Revert // Revert
testClientRep.getAttributes().put(Constants.DEFAULT_ACR_VALUES, null); testClientRep.getAttributes().put(Constants.DEFAULT_ACR_VALUES, null);
@ -413,6 +440,183 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
testRealm().update(realmRep); testRealm().update(realmRep);
} }
// After initial authentication with "acr=2", there will be further re-authentication requests sent in different intervals
// without "acr" parameter. User should be always re-authenticated due SSO, but with different acr levels due their gradual expirations
@Test
public void testMaxAgeConditionWithSSO() {
reconfigureStepUpFlow(300, 300, 200);
// Authentication
openLoginFormWithAcrClaim(true, "3");
authenticateWithUsernamePassword();
authenticateWithTotp();
authenticateWithButton();
assertLoggedInWithAcr("3");
// Re-auth 1: Should be automatically authenticated and still return "3"
oauth.claims(null);
oauth.openLoginForm();
assertLoggedInWithAcr("3");
// Time offset to 210
setTimeOffset(210);
// Re-auth 2: Should return level 2 (gold) due level 3 expired
oauth.openLoginForm();
assertLoggedInWithAcr("gold");
// Time offset to 310
setTimeOffset(310);
// Re-auth 3: Should return level 0 (copper) due levels 1 and 2 expired
oauth.openLoginForm();
assertLoggedInWithAcr("copper");
}
// After initial authentication with "acr=2", there will be further re-authentication requests sent in different intervals
// asking for "acr=2" . User should be asked for re-authentication with various authenticators in various cases.
@Test
public void testMaxAgeConditionWithAcr() {
reconfigureStepUpFlow(300, 200, 200);
// Authentication
openLoginFormWithAcrClaim(true, "3");
authenticateWithUsernamePassword();
authenticateWithTotp();
authenticateWithButton();
assertLoggedInWithAcr("3");
// Re-auth 1: Should be automatically authenticated and still return "3"
openLoginFormWithAcrClaim(true, "3");
assertLoggedInWithAcr("3");
// Time offset to 210
setTimeOffset(210);
// Re-auth 2: Should ask user for re-authentication with level2 and level3. Level1 did not yet expired and should be automatic
openLoginFormWithAcrClaim(true, "3");
authenticateWithTotp();
authenticateWithButton();
assertLoggedInWithAcr("3");
// Time offset to 310
setTimeOffset(310);
// Re-auth 3: Should ask user for re-authentication with level1. Level2 and Level3 did not yet expired and should be automatic
openLoginFormWithAcrClaim(true, "3");
reauthenticateWithPassword();
assertLoggedInWithAcr("3");
}
// Authenticate with LoA=3 and then send request with "prompt=login" to force re-authentication
@Test
public void testMaxAgeConditionWithForcedReauthentication() {
reconfigureStepUpFlow(300, 300, 300);
// Authentication
openLoginFormWithAcrClaim(true, "3");
authenticateWithUsernamePassword();
authenticateWithTotp();
authenticateWithButton();
assertLoggedInWithAcr("3");
// Send request with prompt=login . User should be asked to re-authenticate with level 1
oauth.claims(null);
oauth.prompt(OIDCLoginProtocol.PROMPT_VALUE_LOGIN);
oauth.openLoginForm();
reauthenticateWithPassword();
assertLoggedInWithAcr("silver");
// Request with prompt=login together with "acr=2" . User should be asked to re-authenticate with level 2
openLoginFormWithAcrClaim(true, "gold");
reauthenticateWithPassword();
authenticateWithTotp();
assertLoggedInWithAcr("gold");
// Request with "acr=3", but without prompt. User should be automatically authenticated
oauth.prompt(null);
openLoginFormWithAcrClaim(true, "3");
assertLoggedInWithAcr("3");
}
@Test
public void testChangingLoaConditionConfiguration() {
// Authentication
oauth.openLoginForm();
authenticateWithUsernamePassword();
assertLoggedInWithAcr("silver");
setTimeOffset(120);
// Change condition configuration to 60
reconfigureStepUpFlow(60, 0, 0);
// Re-authenticate without "acr". Should return 0 (copper) due the SSO. Level "silver" should not be returned due it is expired
// based on latest condition configuration
oauth.openLoginForm();
assertLoggedInWithAcr("copper");
// Re-authenticate with requested ACR=1 (silver). User should be asked to re-authenticate
openLoginFormWithAcrClaim(true, "silver");
reauthenticateWithPassword();
assertLoggedInWithAcr("silver");
}
// Backwards compatibility with Keycloak 17 when condition was configured with option "Store Loa in User Session"
@Test
public void testBackwardsCompatibilityForLoaConditionConfig() {
// Reconfigure to the format of Keycloak 17 with option "Store Loa in User Session"
BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow");
final String newFlowAlias = "browser - Level of Authentication FLow";
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
testingClient.server(TEST_REALM_NAME)
.run(session -> FlowUtil.inCurrentRealm(session).selectFlow(newFlowAlias).inForms(forms -> forms.clear()
// level 1 authentication
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
config -> {
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1");
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "true");
});
// username input for level 1
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID);
})
// level 2 authentication
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
config -> {
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "2");
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "false");
});
// password required for level 2
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, OTPFormAuthenticatorFactory.PROVIDER_ID);
})
// level 3 authentication
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
config -> {
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "3");
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "false");
});
// simply push button for level 3
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PushButtonAuthenticatorFactory.PROVIDER_ID);
})
).defineAsBrowserFlow());
// Tests that re-authentication always needed for levels 2 and 3
stepupAuthentication();
}
public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) { public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) {
openLoginFormWithAcrClaim(oauth, essential, acrValues); openLoginFormWithAcrClaim(oauth, essential, acrValues);
} }
@ -429,14 +633,20 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
oauth.openLoginForm(); oauth.openLoginForm();
} }
private void authenticateWithUsername() { private void authenticateWithUsernamePassword() {
loginUsernameOnlyPage.assertCurrent(); loginPage.assertCurrent();
loginUsernameOnlyPage.login("test-user@localhost"); loginPage.login("test-user@localhost", "password");
} }
private void authenticateWithPassword() { private void reauthenticateWithPassword() {
passwordPage.assertCurrent(); loginPage.assertCurrent();
passwordPage.login("password"); Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername());
loginPage.login("password");
}
private void authenticateWithTotp() {
loginTotpPage.assertCurrent();
loginTotpPage.login(totp.generateTOTP("totpSecret"));
} }
private void authenticateWithButton() { private void authenticateWithButton() {

View file

@ -1349,10 +1349,10 @@ otp-supported-applications=Supported Applications
otp-supported-applications.tooltip=Applications that are known to work with the current OTP policy otp-supported-applications.tooltip=Applications that are known to work with the current OTP policy
loa-level=Level of Authentication loa-level=Level of Authentication
loa-level.tooltip=Sets the Level of Authentication to the specified value. loa-level.tooltip=Sets the Level of Authentication to the specified value.
loa-store-in-user-session=Store LoA in user session loa-max-age=Max Age
loa-store-in-user-session.tooltip=Additionally stores the LoA in the user session. If true, it means that subsequent authentications in same browser will see this level and they won't repeat authenticating the user with this level. If this is false, it means that subsequent authentications won't see this level and hence they will repeat authentications for this level again. This is useful if some functionality in the application (e.g. send payment) always require authentication with the particular level. loa-max-age.tooltip=Maximum age in seconds for which this level is considered valid after successful authentication. For example if this is set to 300 and user authenticated with this level and then tries to authenticate again in less than 300 second, then this level will be automatically considered as authenticated without need of user to re-authenticate. If it is set to 0, then the authenticated level is valid just for this authentication and next authentication will always need to re-authenticate. Default value is 10 hours, which is same as default SSO session timeout and it means that level is valid until end of SSO session and user doesn't need to re-authenticate.
loa-condition-level=Level of Authentication (LoA) loa-condition-level=Level of Authentication (LoA)
loa-condition-level.tooltip=The condition evaluates to true if user does not yet have this authentication level and this level is requested. This level of authentication will be set to the session after the subflow, where this condition is configured, is successfully finished. loa-condition-level.tooltip=The number value, usually 1 or bigger, which specifies level of authentication. Condition evaluates to true if user does not yet have this authentication level and this level is requested. This level of authentication will be set to the session after the subflow, where this condition is configured, is successfully finished.
table-of-password-policies=Table of Password Policies table-of-password-policies=Table of Password Policies
add-policy.placeholder=Add policy... add-policy.placeholder=Add policy...
policy-type=Policy Type policy-type=Policy Type