Replace 'Store LoA in User Session' with 'Max Age'. Refactoring of step-up authentications related to that.
Closes #10205
This commit is contained in:
parent
2bae2d2167
commit
93bba8e338
16 changed files with 702 additions and 168 deletions
|
@ -131,7 +131,13 @@ public final class Constants {
|
|||
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";
|
||||
|
||||
// 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 FORCE_LEVEL_OF_AUTHENTICATION = "force-level-of-authentication";
|
||||
public static final String ACR_LOA_MAP = "acr.loa.map";
|
||||
|
|
|
@ -19,11 +19,7 @@ package org.keycloak.authentication;
|
|||
|
||||
import com.google.common.collect.Sets;
|
||||
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.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
@ -32,10 +28,9 @@ import org.keycloak.utils.StringUtil;
|
|||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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;
|
||||
|
||||
public class AuthenticatorUtil {
|
||||
|
@ -50,28 +45,8 @@ public class AuthenticatorUtil {
|
|||
return "true".equals(authSession.getAuthNote(SSO_AUTH));
|
||||
}
|
||||
|
||||
public static boolean isLevelOfAuthenticationForced(AuthenticationSessionModel authSession) {
|
||||
return Boolean.parseBoolean(authSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION));
|
||||
}
|
||||
|
||||
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);
|
||||
public static boolean isForcedReauthentication(AuthenticationSessionModel authSession) {
|
||||
return "true".equals(authSession.getAuthNote(FORCED_REAUTHENTICATION));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -135,34 +110,4 @@ public class AuthenticatorUtil {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ package org.keycloak.authentication.authenticators.browser;
|
|||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
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.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -49,25 +49,31 @@ public class CookieAuthenticator implements Authenticator {
|
|||
} else {
|
||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||
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());
|
||||
AcrStore acrStore = new AcrStore(authSession);
|
||||
|
||||
// Cookie re-authentication is skipped if re-authentication is required
|
||||
if (protocol.requireReauthentication(authResult.getSession(), authSession)) {
|
||||
// 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.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 {
|
||||
// Cookie only authentication, no loa is returned
|
||||
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA));
|
||||
authSession.setAuthNote(AuthenticationManager.SSO_AUTH, "true");
|
||||
context.attachUserSession(authResult.getSession());
|
||||
context.success();
|
||||
int previouslyAuthenticatedLevel = acrStore.getHighestAuthenticatedLevelFromPreviousAuthentication();
|
||||
if (acrStore.getRequestedLevelOfAuthentication() > previouslyAuthenticatedLevel) {
|
||||
// Step-up authentication, we keep the loa from the existing user session.
|
||||
// The cookie alone is not enough and other authentications must follow.
|
||||
acrStore.setLevelAuthenticatedToCurrentRequest(previouslyAuthenticatedLevel);
|
||||
context.attempted();
|
||||
} else {
|
||||
// Cookie only authentication
|
||||
acrStore.setLevelAuthenticatedToCurrentRequest(previouslyAuthenticatedLevel);
|
||||
authSession.setAuthNote(AuthenticationManager.SSO_AUTH, "true");
|
||||
context.attachUserSession(authResult.getSession());
|
||||
context.success();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ import org.keycloak.authentication.AuthenticationFlowContext;
|
|||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
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.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -32,6 +34,11 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
|||
|
||||
public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, AuthenticationFlowCallback {
|
||||
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";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ConditionalLoaAuthenticator.class);
|
||||
|
@ -45,59 +52,77 @@ public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, Au
|
|||
@Override
|
||||
public boolean matchCondition(AuthenticationFlowContext context) {
|
||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||
int currentLoa = AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession);
|
||||
int requestedLoa = AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession);
|
||||
AcrStore acrStore = new AcrStore(authSession);
|
||||
int currentAuthenticationLoa = acrStore.getLevelOfAuthenticationFromCurrentAuthentication();
|
||||
Integer configuredLoa = getConfiguredLoa(context);
|
||||
boolean result = (currentLoa < Constants.MINIMUM_LOA && requestedLoa < Constants.MINIMUM_LOA)
|
||||
|| ((configuredLoa == null || currentLoa < configuredLoa) && currentLoa < requestedLoa);
|
||||
if (configuredLoa == null) configuredLoa = Constants.MINIMUM_LOA;
|
||||
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",
|
||||
context.getAuthenticatorConfig().getAlias(), currentLoa, requestedLoa, configuredLoa, result);
|
||||
|
||||
return result;
|
||||
return !previouslyAuthenticated;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onParentFlowSuccess(AuthenticationFlowContext context) {
|
||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||
AcrStore acrStore = new AcrStore(authSession);
|
||||
|
||||
Integer newLoa = getConfiguredLoa(context);
|
||||
if (newLoa == null) {
|
||||
return;
|
||||
}
|
||||
logger.tracef("Updating LoA to '%d' when authenticating session '%s'", newLoa, authSession.getParentSession().getId());
|
||||
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(newLoa));
|
||||
if (isStoreInUserSession(context)) {
|
||||
authSession.setUserSessionNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(newLoa));
|
||||
int maxAge = getMaxAge(context);
|
||||
if (maxAge == 0) {
|
||||
logger.tracef("Skip updating authenticated level '%d' in condition '%s' for future authentications due max-age set to 0", newLoa, context.getAuthenticatorConfig().getAlias());
|
||||
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
|
||||
public void onTopFlowSuccess() {
|
||||
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",
|
||||
AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession), AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession));
|
||||
acrStore.getRequestedLevelOfAuthentication(), acrStore.getLevelOfAuthenticationFromCurrentAuthentication());
|
||||
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) {
|
||||
try {
|
||||
return Integer.parseInt(context.getAuthenticatorConfig().getConfig().get(LEVEL));
|
||||
} catch (NullPointerException | NumberFormatException e) {
|
||||
logger.errorv("Invalid configuration: {0}", LEVEL);
|
||||
return null;
|
||||
}
|
||||
return LoAUtil.getLevelFromLoaConditionConfiguration(context.getAuthenticatorConfig());
|
||||
}
|
||||
|
||||
private boolean isStoreInUserSession(AuthenticationFlowContext context) {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
private int getMaxAge(AuthenticationFlowContext context) {
|
||||
return LoAUtil.getMaxAgeFromLoaConditionConfiguration(context.getAuthenticatorConfig());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -43,11 +43,11 @@ public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthentica
|
|||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property()
|
||||
.name(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION)
|
||||
.label(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION)
|
||||
.helpText(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION + ".tooltip")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("true")
|
||||
.name(ConditionalLoaAuthenticator.MAX_AGE)
|
||||
.label(ConditionalLoaAuthenticator.MAX_AGE)
|
||||
.helpText(ConditionalLoaAuthenticator.MAX_AGE + ".tooltip")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(ConditionalLoaAuthenticator.DEFAULT_MAX_AGE) // 10 hours
|
||||
.add()
|
||||
.build();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,9 +19,9 @@ package org.keycloak.protocol.oidc;
|
|||
|
||||
import com.google.common.collect.Streams;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.AuthenticatorUtil;
|
||||
import org.keycloak.authentication.ClientAuthenticator;
|
||||
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.util.LoAUtil;
|
||||
import org.keycloak.crypto.CekManagementProvider;
|
||||
import org.keycloak.crypto.ClientSignatureVerifierProvider;
|
||||
import org.keycloak.crypto.ContentEncryptionProvider;
|
||||
|
@ -29,7 +29,6 @@ import org.keycloak.crypto.SignatureProvider;
|
|||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.models.CibaConfig;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
||||
|
@ -63,7 +62,6 @@ import java.util.Collection;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -264,7 +262,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
List<String> result = new ArrayList<>(realmAcrLoaMap.keySet());
|
||||
|
||||
// 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)
|
||||
.collect(Collectors.toList()));
|
||||
return result;
|
||||
|
|
|
@ -24,7 +24,8 @@ import org.keycloak.OAuth2Constants;
|
|||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.TokenCategory;
|
||||
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.provider.IdentityBrokerException;
|
||||
import org.keycloak.cluster.ClusterProvider;
|
||||
|
@ -565,7 +566,7 @@ public class TokenManager {
|
|||
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());
|
||||
|
||||
// Remove authentication session now
|
||||
|
@ -901,7 +902,7 @@ public class TokenManager {
|
|||
}
|
||||
|
||||
private String getAcr(AuthenticatedClientSessionModel clientSession) {
|
||||
int loa = AuthenticatorUtil.getCurrentLevelOfAuthentication(clientSession);
|
||||
int loa = LoAUtil.getCurrentLevelOfAuthentication(clientSession);
|
||||
if (loa < Constants.MINIMUM_LOA) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ public class AcrUtils {
|
|||
public static String mapLoaToAcr(int loa, Map<String, Integer> acrLoaMap, Collection<String> acrValues) {
|
||||
String acr = null;
|
||||
if (!acrLoaMap.isEmpty() && !acrValues.isEmpty()) {
|
||||
int maxLoa = 0;
|
||||
int maxLoa = -1;
|
||||
for (String acrValue : acrValues) {
|
||||
Integer mappedLoa = acrLoaMap.get(acrValue);
|
||||
// if there is no mapping for the acrValue, it may be an integer itself
|
||||
|
|
|
@ -143,6 +143,9 @@ public class AuthenticationManager {
|
|||
// clientSession note with flag that clientSession was authenticated through SSO cookie
|
||||
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);
|
||||
|
||||
public static final String FORM_USERNAME = "username";
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.validation;
|
||||
|
||||
import org.keycloak.authentication.AuthenticatorUtil;
|
||||
import org.keycloak.authentication.authenticators.util.LoAUtil;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.protocol.ProtocolMapperConfigException;
|
||||
|
@ -279,7 +280,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
|
|||
}
|
||||
for (String configuredAcr : defaultAcrValues) {
|
||||
if (acrToLoaMap.containsKey(configuredAcr)) continue;
|
||||
if (!AuthenticatorUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm())
|
||||
if (!LoAUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm())
|
||||
.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");
|
||||
}
|
||||
|
|
|
@ -166,6 +166,8 @@ public class OAuthClient {
|
|||
|
||||
private String maxAge;
|
||||
|
||||
private String prompt;
|
||||
|
||||
private String responseType;
|
||||
|
||||
private String responseMode;
|
||||
|
@ -255,6 +257,7 @@ public class OAuthClient {
|
|||
clientSessionState = null;
|
||||
clientSessionHost = null;
|
||||
maxAge = null;
|
||||
prompt = null;
|
||||
responseType = OAuth2Constants.CODE;
|
||||
responseMode = null;
|
||||
nonce = null;
|
||||
|
@ -1133,6 +1136,9 @@ public class OAuthClient {
|
|||
if (maxAge != null) {
|
||||
parameters.add(new BasicNameValuePair(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge));
|
||||
}
|
||||
if (prompt != null) {
|
||||
parameters.add(new BasicNameValuePair(OIDCLoginProtocol.PROMPT_PARAM, prompt));
|
||||
}
|
||||
if (request != null) {
|
||||
parameters.add(new BasicNameValuePair(OIDCLoginProtocol.REQUEST_PARAM, request));
|
||||
}
|
||||
|
@ -1418,6 +1424,9 @@ public class OAuthClient {
|
|||
if (maxAge != null) {
|
||||
b.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
|
||||
}
|
||||
if (prompt != null) {
|
||||
b.queryParam(OIDCLoginProtocol.PROMPT_PARAM, prompt);
|
||||
}
|
||||
if (request != null) {
|
||||
b.queryParam(OIDCLoginProtocol.REQUEST_PARAM, request);
|
||||
}
|
||||
|
@ -1622,6 +1631,11 @@ public class OAuthClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
public OAuthClient prompt(String prompt) {
|
||||
this.prompt = prompt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuthClient responseType(String responseType) {
|
||||
this.responseType = responseType;
|
||||
return this;
|
||||
|
|
|
@ -93,7 +93,7 @@ public class AuthenticationFlowCallbackProviderTest extends AbstractTestRealmKey
|
|||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
||||
config -> {
|
||||
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)
|
||||
)
|
||||
|
|
|
@ -32,19 +32,22 @@ import org.junit.Before;
|
|||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.ClaimsRepresentation;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
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.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||
import org.keycloak.testsuite.pages.PasswordPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.LoginTotpPage;
|
||||
import org.keycloak.testsuite.pages.PushTheButtonPage;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
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 static org.hamcrest.CoreMatchers.is;
|
||||
|
@ -74,10 +80,12 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Page
|
||||
protected LoginUsernameOnlyPage loginUsernameOnlyPage;
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected PasswordPage passwordPage;
|
||||
protected LoginTotpPage loginTotpPage;
|
||||
|
||||
private TimeBasedOTP totp = new TimeBasedOTP();
|
||||
|
||||
@Page
|
||||
protected PushTheButtonPage pushTheButtonPage;
|
||||
|
@ -89,6 +97,10 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
try {
|
||||
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) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -108,6 +120,10 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
|
||||
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";
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||
testingClient.server(TEST_REALM_NAME)
|
||||
|
@ -117,26 +133,32 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
||||
config -> {
|
||||
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
|
||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID);
|
||||
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 -> {
|
||||
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "2");
|
||||
config.getConfig().put(ConditionalLoaAuthenticator.MAX_AGE, String.valueOf(maxAge2));
|
||||
});
|
||||
|
||||
// password required for level 2
|
||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PasswordFormFactory.PROVIDER_ID);
|
||||
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 -> {
|
||||
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "3");
|
||||
config.getConfig().put(ConditionalLoaAuthenticator.MAX_AGE, String.valueOf(maxAge3));
|
||||
});
|
||||
|
||||
// simply push button for level 3
|
||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PushButtonAuthenticatorFactory.PROVIDER_ID);
|
||||
|
@ -145,6 +167,11 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
).defineAsBrowserFlow());
|
||||
}
|
||||
|
||||
private void reconfigureStepUpFlow(int maxAge1, int maxAge2, int maxAge3) {
|
||||
BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow");
|
||||
configureStepUpFlow(testingClient, maxAge1, maxAge2, maxAge3);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
BrowserFlowTest.revertFlows(testRealm(), "browser - Level of Authentication FLow");
|
||||
|
@ -154,7 +181,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
public void loginWithoutAcr() {
|
||||
oauth.openLoginForm();
|
||||
// Authentication without specific LOA results in level 1 authentication
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
}
|
||||
|
||||
|
@ -162,7 +189,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
public void loginWithAcr1() {
|
||||
// username input for level 1
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
|
||||
}
|
||||
|
@ -171,8 +198,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
public void loginWithAcr2() {
|
||||
// username and password input for level 2
|
||||
openLoginFormWithAcrClaim(true, "gold");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
assertLoggedInWithAcr("gold");
|
||||
}
|
||||
|
||||
|
@ -180,8 +207,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
public void loginWithAcr3() {
|
||||
// username, password input and finally push button for level 3
|
||||
openLoginFormWithAcrClaim(true, "3");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
authenticateWithButton();
|
||||
// ACR 3 is returned because it was requested, although there is no mapping for it
|
||||
assertLoggedInWithAcr("3");
|
||||
|
@ -191,15 +218,15 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
public void stepupAuthentication() {
|
||||
// logging in to level 1
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
// doing step-up authentication to level 2
|
||||
openLoginFormWithAcrClaim(true, "gold");
|
||||
authenticateWithPassword();
|
||||
authenticateWithTotp();
|
||||
assertLoggedInWithAcr("gold");
|
||||
// step-up to level 3 needs password authentication because level 2 is not stored in user session
|
||||
openLoginFormWithAcrClaim(true, "3");
|
||||
authenticateWithPassword();
|
||||
authenticateWithTotp();
|
||||
authenticateWithButton();
|
||||
assertLoggedInWithAcr("3");
|
||||
}
|
||||
|
@ -207,7 +234,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
@Test
|
||||
public void stepupToUnknownEssentialAcrFails() {
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
// step-up to unknown acr
|
||||
openLoginFormWithAcrClaim(true, "uranium");
|
||||
|
@ -217,36 +244,36 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
@Test
|
||||
public void reauthenticationWithNoAcr() {
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
oauth.claims(null);
|
||||
oauth.openLoginForm();
|
||||
assertLoggedInWithAcr("0");
|
||||
assertLoggedInWithAcr("silver"); // Return silver without need to re-authenticate due maxAge for "silver" condition did not timed-out yet
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reauthenticationWithReachedAcr() {
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
assertLoggedInWithAcr("0");
|
||||
assertLoggedInWithAcr("silver"); // Return previous level due maxAge for "silver" condition did not timed-out yet
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reauthenticationWithOptionalUnknownAcr() {
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
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
|
||||
public void essentialClaimNotReachedFails() {
|
||||
openLoginFormWithAcrClaim(true, "4");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
authenticateWithButton();
|
||||
assertErrorPage("Authentication requirements not fulfilled");
|
||||
}
|
||||
|
@ -254,8 +281,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
@Test
|
||||
public void optionalClaimNotReachedSucceeds() {
|
||||
openLoginFormWithAcrClaim(false, "4");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
authenticateWithButton();
|
||||
// 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
|
||||
|
@ -271,7 +298,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
@Test
|
||||
public void optionalUnknownClaimSucceeds() {
|
||||
openLoginFormWithAcrClaim(false, "iron");
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
}
|
||||
|
||||
|
@ -280,24 +307,24 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
driver.navigate().to(UriBuilder.fromUri(oauth.getLoginFormUrl())
|
||||
.queryParam("acr_values", "gold 3")
|
||||
.build().toString());
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
assertLoggedInWithAcr("gold");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleEssentialAcrValues() {
|
||||
openLoginFormWithAcrClaim(true, "gold", "3");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
assertLoggedInWithAcr("gold");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleOptionalAcrValues() {
|
||||
openLoginFormWithAcrClaim(false, "gold", "3");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
assertLoggedInWithAcr("gold");
|
||||
}
|
||||
|
||||
|
@ -320,8 +347,8 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
testClient.update(testClientRep);
|
||||
|
||||
openLoginFormWithAcrClaim(true, "realm:gold");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithUsernamePassword();
|
||||
authenticateWithTotp();
|
||||
assertLoggedInWithAcr("realm:gold");
|
||||
|
||||
// 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");
|
||||
|
||||
openLoginFormWithAcrClaim(true, "gold");
|
||||
authenticateWithPassword();
|
||||
authenticateWithTotp();
|
||||
assertLoggedInWithAcr("gold");
|
||||
|
||||
// Rollback
|
||||
|
@ -349,23 +376,23 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
// Should request client to authenticate with silver
|
||||
oauth.openLoginForm();
|
||||
authenticateWithUsername();
|
||||
authenticateWithUsernamePassword();
|
||||
assertLoggedInWithAcr("silver");
|
||||
|
||||
// Re-configure to level gold
|
||||
OIDCAdvancedConfigWrapper.fromClientRepresentation(testClientRep).setAttributeMultivalued(Constants.DEFAULT_ACR_VALUES, Arrays.asList("gold"));
|
||||
testClient.update(testClientRep);
|
||||
oauth.openLoginForm();
|
||||
authenticateWithPassword();
|
||||
authenticateWithTotp();
|
||||
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");
|
||||
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");
|
||||
assertLoggedInWithAcr("0");
|
||||
assertLoggedInWithAcr("silver");
|
||||
|
||||
// Revert
|
||||
testClientRep.getAttributes().put(Constants.DEFAULT_ACR_VALUES, null);
|
||||
|
@ -413,6 +440,183 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
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) {
|
||||
openLoginFormWithAcrClaim(oauth, essential, acrValues);
|
||||
}
|
||||
|
@ -429,14 +633,20 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
oauth.openLoginForm();
|
||||
}
|
||||
|
||||
private void authenticateWithUsername() {
|
||||
loginUsernameOnlyPage.assertCurrent();
|
||||
loginUsernameOnlyPage.login("test-user@localhost");
|
||||
private void authenticateWithUsernamePassword() {
|
||||
loginPage.assertCurrent();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
}
|
||||
|
||||
private void authenticateWithPassword() {
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login("password");
|
||||
private void reauthenticateWithPassword() {
|
||||
loginPage.assertCurrent();
|
||||
Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername());
|
||||
loginPage.login("password");
|
||||
}
|
||||
|
||||
private void authenticateWithTotp() {
|
||||
loginTotpPage.assertCurrent();
|
||||
loginTotpPage.login(totp.generateTOTP("totpSecret"));
|
||||
}
|
||||
|
||||
private void authenticateWithButton() {
|
||||
|
|
|
@ -1349,10 +1349,10 @@ otp-supported-applications=Supported Applications
|
|||
otp-supported-applications.tooltip=Applications that are known to work with the current OTP policy
|
||||
loa-level=Level of Authentication
|
||||
loa-level.tooltip=Sets the Level of Authentication to the specified value.
|
||||
loa-store-in-user-session=Store LoA in user session
|
||||
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=Max Age
|
||||
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.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
|
||||
add-policy.placeholder=Add policy...
|
||||
policy-type=Policy Type
|
||||
|
|
Loading…
Reference in a new issue