Show error in case of an unkown essential acr claim. Make sure correc… (#10088)
* Show error in case of an unkown essential acr claim. Make sure correct acr is set after authentication flow during step-up authentication Closes #8724 Co-authored-by: Cornelia Lahnsteiner <cornelia.lahnsteiner@prime-sign.com> Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
parent
589606b1c1
commit
90d4e586b6
22 changed files with 622 additions and 68 deletions
|
@ -36,4 +36,11 @@ public interface AuthenticationFlowCallback extends Authenticator {
|
|||
*/
|
||||
void onParentFlowSuccess(AuthenticationFlowContext context);
|
||||
|
||||
|
||||
/**
|
||||
* Triggered after the top authentication flow is successfully finished.
|
||||
* It is really suitable for last verification of successful authentication
|
||||
*/
|
||||
default void onTopFlowSuccess() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,6 +136,5 @@ public final class Constants {
|
|||
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 int MINIMUM_LOA = 0;
|
||||
public static final int MAXIMUM_LOA = Integer.MAX_VALUE;
|
||||
public static final int NO_LOA = -1;
|
||||
}
|
||||
|
|
|
@ -710,18 +710,6 @@ public class AuthenticationProcessor {
|
|||
return status == AuthenticationSessionModel.ExecutionStatus.SUCCESS;
|
||||
}
|
||||
|
||||
public boolean isEvaluatedTrue(AuthenticationExecutionModel model) {
|
||||
AuthenticationSessionModel.ExecutionStatus status = authenticationSession.getExecutionStatus().get(model.getId());
|
||||
if (status == null) return false;
|
||||
return status == AuthenticationSessionModel.ExecutionStatus.EVALUATED_TRUE;
|
||||
}
|
||||
|
||||
public boolean isEvaluatedFalse(AuthenticationExecutionModel model) {
|
||||
AuthenticationSessionModel.ExecutionStatus status = authenticationSession.getExecutionStatus().get(model.getId());
|
||||
if (status == null) return false;
|
||||
return status == AuthenticationSessionModel.ExecutionStatus.EVALUATED_FALSE;
|
||||
}
|
||||
|
||||
public Response handleBrowserExceptionList(AuthenticationFlowException e) {
|
||||
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authenticationSession);
|
||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||
|
|
|
@ -200,7 +200,7 @@ class AuthenticationSelectionResolver {
|
|||
|
||||
// For conditional execution, we must check if condition is true. Otherwise return false, which means trying next
|
||||
// requiredExecution in the list
|
||||
return !flow.isConditionalSubflowDisabled(ex, false);
|
||||
return !flow.isConditionalSubflowDisabled(ex);
|
||||
|
||||
}).findFirst().orElse(null);
|
||||
|
||||
|
|
|
@ -17,13 +17,27 @@
|
|||
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.keycloak.services.managers.AuthenticationManager.SSO_AUTH;
|
||||
|
||||
public class AuthenticatorUtil {
|
||||
|
||||
// It is used for identification of note included in authentication session for storing callback provider factories
|
||||
public static String CALLBACKS_FACTORY_IDS_NOTE = "callbacksFactoryProviderIds";
|
||||
|
||||
|
||||
public static boolean isSSOAuthentication(AuthenticationSessionModel authSession) {
|
||||
return "true".equals(authSession.getAuthNote(SSO_AUTH));
|
||||
}
|
||||
|
||||
public static boolean isLevelOfAuthenticationForced(AuthenticationSessionModel authSession) {
|
||||
return Boolean.parseBoolean(authSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION));
|
||||
}
|
||||
|
@ -47,4 +61,46 @@ public class AuthenticatorUtil {
|
|||
String clientSessionLoaNote = clientSession.getNote(Constants.LEVEL_OF_AUTHENTICATION);
|
||||
return clientSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(clientSessionLoaNote);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication session note for callbacks defined for {@link AuthenticationFlowCallbackFactory) factories
|
||||
*
|
||||
* @param authSession authentication session
|
||||
* @param authFactoryId authentication factory ID which should be added to the authentication session note
|
||||
*/
|
||||
public static void setAuthCallbacksFactoryIds(AuthenticationSessionModel authSession, String authFactoryId) {
|
||||
if (authSession == null || StringUtil.isBlank(authFactoryId)) return;
|
||||
|
||||
final String callbacksFactories = authSession.getAuthNote(CALLBACKS_FACTORY_IDS_NOTE);
|
||||
|
||||
if (StringUtil.isNotBlank(callbacksFactories)) {
|
||||
boolean containsProviderId = callbacksFactories.equals(authFactoryId) ||
|
||||
callbacksFactories.contains(Constants.CFG_DELIMITER + authFactoryId) ||
|
||||
callbacksFactories.contains(authFactoryId + Constants.CFG_DELIMITER);
|
||||
|
||||
if (!containsProviderId) {
|
||||
authSession.setAuthNote(CALLBACKS_FACTORY_IDS_NOTE, callbacksFactories + Constants.CFG_DELIMITER + authFactoryId);
|
||||
}
|
||||
} else {
|
||||
authSession.setAuthNote(CALLBACKS_FACTORY_IDS_NOTE, authFactoryId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get set of Authentication factories IDs defined in authentication session as CALLBACKS_FACTORY_IDS_NOTE
|
||||
*
|
||||
* @param authSession authentication session
|
||||
* @return set of factories IDs
|
||||
*/
|
||||
public static Set<String> getAuthCallbacksFactoryIds(AuthenticationSessionModel authSession) {
|
||||
if (authSession == null) return Collections.emptySet();
|
||||
|
||||
final String callbacksFactories = authSession.getAuthNote(CALLBACKS_FACTORY_IDS_NOTE);
|
||||
|
||||
if (StringUtil.isNotBlank(callbacksFactories)) {
|
||||
return Sets.newHashSet(callbacksFactories.split(Constants.CFG_DELIMITER));
|
||||
} else {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import javax.ws.rs.HttpMethod;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
@ -35,6 +36,8 @@ import java.util.ArrayList;
|
|||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
@ -48,7 +51,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
private final List<AuthenticationExecutionModel> executions;
|
||||
private final AuthenticationProcessor processor;
|
||||
private final AuthenticationFlowModel flow;
|
||||
private boolean successful;
|
||||
private boolean successful = false;
|
||||
private List<AuthenticationFlowException> afeList = new ArrayList<>();
|
||||
|
||||
public DefaultAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) {
|
||||
|
@ -158,6 +161,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
|
||||
result.setAuthenticationSelections(createAuthenticationSelectionList(model));
|
||||
|
||||
if (factory instanceof AuthenticationFlowCallbackFactory) {
|
||||
AuthenticatorUtil.setAuthCallbacksFactoryIds(processor.getAuthenticationSession(), factory.getId());
|
||||
}
|
||||
|
||||
logger.debugv("action: {0}", model.getAuthenticator());
|
||||
authenticator.action(result);
|
||||
Response response = processResult(result, true);
|
||||
|
@ -250,7 +257,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
AuthenticationExecutionModel required = requiredIListIterator.next();
|
||||
//Conditional flows must be considered disabled (non-existent) if their condition evaluates to false.
|
||||
//If the flow has been processed before it will not be removed to consider its execution status.
|
||||
if (required.isConditional() && !isProcessed(required) && isConditionalSubflowDisabled(required, true)) {
|
||||
if (required.isConditional() && !isProcessed(required) && isConditionalSubflowDisabled(required)) {
|
||||
requiredIListIterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
@ -270,8 +277,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
if (requiredList.isEmpty()) {
|
||||
//check if an alternative is already successful, in case we are returning in the flow after an action
|
||||
if (alternativeList.stream().anyMatch(alternative -> processor.isSuccessful(alternative) || isSetupRequired(alternative))) {
|
||||
successful = true;
|
||||
return null;
|
||||
return onFlowExecutionsSuccessful();
|
||||
}
|
||||
|
||||
//handle alternative elements: the first alternative element to be satisfied is enough
|
||||
|
@ -282,8 +288,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
return response;
|
||||
}
|
||||
if (processor.isSuccessful(alternative) || isSetupRequired(alternative)) {
|
||||
successful = true;
|
||||
return null;
|
||||
return onFlowExecutionsSuccessful();
|
||||
}
|
||||
} catch (AuthenticationFlowException afe) {
|
||||
//consuming the error is not good here from an administrative point of view, but the user, since he has alternatives, should be able to go to another alternative and continue
|
||||
|
@ -292,7 +297,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
successful = requiredElementsSuccessful;
|
||||
if (requiredElementsSuccessful) {
|
||||
return onFlowExecutionsSuccessful();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -326,10 +333,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
/**
|
||||
* Checks if the conditional subflow passed in parameter is disabled.
|
||||
* @param model
|
||||
* @param storeResult whether to store the result of the conditional evaluations
|
||||
* @return
|
||||
*/
|
||||
boolean isConditionalSubflowDisabled(AuthenticationExecutionModel model, boolean storeResult) {
|
||||
boolean isConditionalSubflowDisabled(AuthenticationExecutionModel model) {
|
||||
if (model == null || !model.isAuthenticatorFlow() || !model.isConditional()) {
|
||||
return false;
|
||||
};
|
||||
|
@ -339,8 +345,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
.filter(this::isConditionalAuthenticator)
|
||||
.filter(s -> s.isEnabled())
|
||||
.collect(Collectors.toList());
|
||||
return conditionalAuthenticatorList.isEmpty() || conditionalAuthenticatorList.stream()
|
||||
.anyMatch(m -> conditionalNotMatched(m, modelList, storeResult));
|
||||
boolean conditionalSubflowDisabled = conditionalAuthenticatorList.isEmpty() || conditionalAuthenticatorList.stream()
|
||||
.anyMatch(m -> conditionalNotMatched(m, modelList));
|
||||
logger.tracef("Conditional subflow '%s' is %s", logExecutionAlias(model), conditionalSubflowDisabled ? "disabled" : "enabled");
|
||||
return conditionalSubflowDisabled;
|
||||
}
|
||||
|
||||
private boolean isConditionalAuthenticator(AuthenticationExecutionModel model) {
|
||||
|
@ -355,25 +363,16 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
return factory;
|
||||
}
|
||||
|
||||
private boolean conditionalNotMatched(AuthenticationExecutionModel model, List<AuthenticationExecutionModel> executionList, boolean storeResult) {
|
||||
private boolean conditionalNotMatched(AuthenticationExecutionModel model, List<AuthenticationExecutionModel> executionList) {
|
||||
AuthenticatorFactory factory = getAuthenticatorFactory(model);
|
||||
ConditionalAuthenticator authenticator = (ConditionalAuthenticator) createAuthenticator(factory);
|
||||
AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executionList);
|
||||
|
||||
boolean matchCondition;
|
||||
|
||||
// Retrieve previous evaluation result if any, else evaluate and store result for future re-evaluation
|
||||
if (processor.isEvaluatedTrue(model)) {
|
||||
matchCondition = true;
|
||||
} else if (processor.isEvaluatedFalse(model)) {
|
||||
matchCondition = false;
|
||||
} else {
|
||||
matchCondition = authenticator.matchCondition(context);
|
||||
if (storeResult) {
|
||||
setExecutionStatus(model,
|
||||
matchCondition ? AuthenticationSessionModel.ExecutionStatus.EVALUATED_TRUE : AuthenticationSessionModel.ExecutionStatus.EVALUATED_FALSE);
|
||||
}
|
||||
}
|
||||
// Always store result for future re-evaluation. It is a chance that some condition is evaluated multiple times during the flow,
|
||||
// but this is expected as "conditions of condition" can be changed during the flow (EG. when acr level is reached or when user is added to the context)
|
||||
boolean matchCondition = authenticator.matchCondition(context);
|
||||
setExecutionStatus(model,
|
||||
matchCondition ? AuthenticationSessionModel.ExecutionStatus.EVALUATED_TRUE : AuthenticationSessionModel.ExecutionStatus.EVALUATED_FALSE);
|
||||
|
||||
return !matchCondition;
|
||||
}
|
||||
|
@ -547,6 +546,8 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
private void setExecutionStatus(AuthenticationExecutionModel authExecutionModel, CommonClientSessionModel.ExecutionStatus status) {
|
||||
this.processor.getAuthenticationSession().setExecutionStatus(authExecutionModel.getId(), status);
|
||||
|
||||
logger.tracef("Set execution status: Execution: %s, status: %s", logExecutionAlias(authExecutionModel), status);
|
||||
|
||||
if (authExecutionModel.isAuthenticatorFlow() && status == CommonClientSessionModel.ExecutionStatus.SUCCESS) {
|
||||
// Trigger callbacks after flow was successfully finished
|
||||
processor.getRealm().getAuthenticationExecutionsStream(authExecutionModel.getFlowId()).forEach(this::checkAuthCallback);
|
||||
|
@ -563,8 +564,37 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
AuthenticationFlowCallback authCallback = (AuthenticationFlowCallback) createAuthenticator(authFactory);
|
||||
logger.tracef("Will trigger callback '%s' after successful finish of the flow '%s'", authFactory.getId(), execution.getParentFlow());
|
||||
authCallback.onParentFlowSuccess(processor.createAuthenticatorContext(execution, authCallback, null)); // no need to have executions filled
|
||||
AuthenticatorUtil.setAuthCallbacksFactoryIds(processor.getAuthenticationSession(), authFactory.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is triggered when current flow is successful due the fact that it's executions passed.
|
||||
// It is opportunity to do some last "generic" checks before considering whole authentication as successful
|
||||
private Response onFlowExecutionsSuccessful() {
|
||||
if (flow.isTopLevel()) {
|
||||
logger.debugf("Authentication successful of the top flow '%s'", flow.getAlias());
|
||||
executeTopFlowSuccessCallbacks();
|
||||
}
|
||||
|
||||
successful = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute callbacks defined for each {@see AuthenticationFlowCallbackFactory} class in top authentication flow if success
|
||||
*/
|
||||
private void executeTopFlowSuccessCallbacks() {
|
||||
final AuthenticationSessionModel authSession = processor.getAuthenticationSession();
|
||||
final Set<String> factoryProviderIDs = AuthenticatorUtil.getAuthCallbacksFactoryIds(authSession);
|
||||
|
||||
factoryProviderIDs.stream()
|
||||
.filter(StringUtil::isNotBlank)
|
||||
.map(id -> processor.getSession().getProvider(Authenticator.class, id))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(AuthenticationFlowCallback.class::isInstance)
|
||||
.map(AuthenticationFlowCallback.class::cast)
|
||||
.forEach(AuthenticationFlowCallback::onTopFlowSuccess);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ public interface ConditionalAuthenticatorFactory extends AuthenticatorFactory, D
|
|||
|
||||
@Override
|
||||
default Authenticator createDisplay(KeycloakSession session, String displayType) {
|
||||
if (displayType == null) return getSingleton();
|
||||
if (displayType == null) return create(session);
|
||||
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||
return getSingleton();
|
||||
return create(session);
|
||||
}
|
||||
|
||||
ConditionalAuthenticator getSingleton();
|
||||
|
|
|
@ -20,28 +20,41 @@ package org.keycloak.authentication.authenticators.conditional;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowCallback;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
import org.keycloak.authentication.AuthenticatorUtil;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, AuthenticationFlowCallback {
|
||||
|
||||
public static final String LEVEL = "loa-condition-level";
|
||||
public static final String STORE_IN_USER_SESSION = "loa-store-in-user-session";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ConditionalLoaAuthenticator.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public ConditionalLoaAuthenticator(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchCondition(AuthenticationFlowContext context) {
|
||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||
int currentLoa = AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession);
|
||||
int requestedLoa = AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession);
|
||||
Integer configuredLoa = getConfiguredLoa(context);
|
||||
return (currentLoa < Constants.MINIMUM_LOA && requestedLoa < Constants.MINIMUM_LOA)
|
||||
boolean result = (currentLoa < Constants.MINIMUM_LOA && requestedLoa < Constants.MINIMUM_LOA)
|
||||
|| ((configuredLoa == null || currentLoa < configuredLoa) && currentLoa < requestedLoa);
|
||||
|
||||
logger.tracef("Checking condition '%s' : currentLoa: %d, requestedLoa: %d, configuredLoa: %d, evaluation result: %b",
|
||||
context.getAuthenticatorConfig().getAlias(), currentLoa, requestedLoa, configuredLoa, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -58,6 +71,17 @@ public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, Au
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTopFlowSuccess() {
|
||||
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
||||
|
||||
if (AuthenticatorUtil.isLevelOfAuthenticationForced(authSession) && !AuthenticatorUtil.isLevelOfAuthenticationSatisfied(authSession) && !AuthenticatorUtil.isSSOAuthentication(authSession)) {
|
||||
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));
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, details, Messages.ACR_NOT_FULFILLED);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer getConfiguredLoa(AuthenticationFlowContext context) {
|
||||
try {
|
||||
return Integer.parseInt(context.getAuthenticatorConfig().getConfig().get(LEVEL));
|
||||
|
|
|
@ -20,7 +20,9 @@ package org.keycloak.authentication.authenticators.conditional;
|
|||
import java.util.List;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationFlowCallbackFactory;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
@ -28,7 +30,6 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
|
|||
public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthenticatorFactory, AuthenticationFlowCallbackFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "conditional-level-of-authentication";
|
||||
private static final ConditionalLoaAuthenticator SINGLETON = new ConditionalLoaAuthenticator();
|
||||
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = new AuthenticationExecutionModel.Requirement[]{
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED
|
||||
|
@ -50,6 +51,11 @@ public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthentica
|
|||
.add()
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return new ConditionalLoaAuthenticator(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) { }
|
||||
|
||||
|
@ -101,6 +107,7 @@ public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthentica
|
|||
|
||||
@Override
|
||||
public ConditionalAuthenticator getSingleton() {
|
||||
return SINGLETON;
|
||||
// NOP - instance created in create() method
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -306,9 +306,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
Integer loa = acrLoaMap.get(acr);
|
||||
return loa == null ? Integer.parseInt(acr) : loa;
|
||||
} catch (NumberFormatException e) {
|
||||
// this is an unknown acr, we assume in case of an essential claim a very high LoA, and a minimum LoA if not essential
|
||||
return Boolean.parseBoolean(authenticationSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION))
|
||||
? Constants.MAXIMUM_LOA : Constants.MINIMUM_LOA;
|
||||
// this is an unknown acr. In case of an essential claim, we directly reject authentication as we cannot met the specification requirement. Otherwise fallback to minimum LoA
|
||||
boolean essential = Boolean.parseBoolean(authenticationSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION));
|
||||
if (essential) {
|
||||
logger.errorf("Requested essential acr value '%s' is not a number and it is not mapped in the client mappings. Please doublecheck your client configuration or correct ACR passed in the 'claims' parameter.", acr);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.CLAIMS_PARAM);
|
||||
} else {
|
||||
logger.warnf("Requested acr value '%s' is not a number and it is not mapped in the client mappings. Please doublecheck your client configuration or correct ACR passed in the 'claims' parameter. Ignoring passed acr", acr);
|
||||
return Constants.MINIMUM_LOA;
|
||||
}
|
||||
}
|
||||
}).min().ifPresent(loa -> authenticationSession.setClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION, String.valueOf(loa)));
|
||||
|
||||
|
|
|
@ -37,26 +37,30 @@ public class AcrUtils {
|
|||
private static final Logger LOGGER = Logger.getLogger(AcrUtils.class);
|
||||
|
||||
public static List<String> getRequiredAcrValues(String claimsParam) {
|
||||
return getAcrValues(claimsParam, null, false);
|
||||
return getAcrValues(claimsParam, null, true);
|
||||
}
|
||||
|
||||
public static List<String> getAcrValues(String claimsParam, String acrValuesParam) {
|
||||
return getAcrValues(claimsParam, acrValuesParam, true);
|
||||
return getAcrValues(claimsParam, acrValuesParam, false);
|
||||
}
|
||||
|
||||
private static List<String> getAcrValues(String claimsParam, String acrValuesParam, boolean notEssential) {
|
||||
private static List<String> getAcrValues(String claimsParam, String acrValuesParam, boolean essential) {
|
||||
List<String> acrValues = new ArrayList<>();
|
||||
if (acrValuesParam != null && notEssential) {
|
||||
if (acrValuesParam != null && !essential) {
|
||||
acrValues.addAll(Arrays.asList(acrValuesParam.split(" ")));
|
||||
}
|
||||
if (claimsParam != null) {
|
||||
try {
|
||||
ClaimsRepresentation claims = JsonSerialization.readValue(claimsParam, ClaimsRepresentation.class);
|
||||
ClaimsRepresentation.ClaimValue<String> acrClaim = claims.getClaimValue(IDToken.ACR, ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
|
||||
if (acrClaim != null) {
|
||||
if (notEssential || acrClaim.isEssential()) {
|
||||
if (acrClaim.getValues() != null) {
|
||||
acrValues.addAll(acrClaim.getValues());
|
||||
if (claims == null) {
|
||||
LOGGER.warnf("Invalid claims parameter. Claims parameter should be JSON");
|
||||
} else {
|
||||
ClaimsRepresentation.ClaimValue<String> acrClaim = claims.getClaimValue(IDToken.ACR, ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
|
||||
if (acrClaim != null) {
|
||||
if (!essential || acrClaim.isEssential()) {
|
||||
if (acrClaim.getValues() != null) {
|
||||
acrValues.addAll(acrClaim.getValues());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +96,7 @@ public class AcrUtils {
|
|||
mappedLoa = Integer.parseInt(acrValue);
|
||||
} catch (NumberFormatException e) {
|
||||
// the acrValue cannot be mapped
|
||||
LOGGER.warnf("Acr value '%s' cannot be mapped to int", acrValue);
|
||||
}
|
||||
}
|
||||
if (mappedLoa != null && mappedLoa > maxLoa && loa >= mappedLoa) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.TokenVerifier.TokenTypeCheck;
|
|||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.AuthenticatorUtil;
|
||||
import org.keycloak.authentication.ConsoleDisplayMode;
|
||||
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||
import org.keycloak.authentication.InitiatedActionSupport;
|
||||
|
@ -919,7 +920,7 @@ public class AuthenticationManager {
|
|||
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
|
||||
|
||||
// Update userSession note with authTime. But just if flag SSO_AUTH is not set
|
||||
boolean isSSOAuthentication = "true".equals(authSession.getAuthNote(SSO_AUTH));
|
||||
boolean isSSOAuthentication = AuthenticatorUtil.isSSOAuthentication(authSession);
|
||||
if (isSSOAuthentication) {
|
||||
clientSession.setNote(SSO_AUTH, "true");
|
||||
authSession.removeAuthNote(SSO_AUTH);
|
||||
|
|
|
@ -243,6 +243,8 @@ public class Messages {
|
|||
|
||||
public static final String BROKER_LINKING_SESSION_EXPIRED = "brokerLinkingSessionExpired";
|
||||
|
||||
public static final String ACR_NOT_FULFILLED = "acrNotFulfilled";
|
||||
|
||||
public static final String PAGE_NOT_FOUND = "pageNotFound";
|
||||
|
||||
public static final String INTERNAL_SERVER_ERROR = "internalServerError";
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.testsuite.authentication;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowCallback;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class CustomAuthenticationFlowCallback implements AuthenticationFlowCallback {
|
||||
|
||||
public static final String EXPECTED_ERROR_MESSAGE = "Custom Authentication Flow Callback message";
|
||||
|
||||
@Override
|
||||
public void onTopFlowSuccess() {
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.GENERIC_AUTHENTICATION_ERROR, "detail", EXPECTED_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onParentFlowSuccess(AuthenticationFlowContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
context.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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.testsuite.authentication;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationFlowCallbackFactory;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class CustomAuthenticationFlowCallbackFactory implements AuthenticationFlowCallbackFactory {
|
||||
public static final String PROVIDER_ID = "custom-callback-authenticator";
|
||||
|
||||
private static final CustomAuthenticationFlowCallback SINGLETON = new CustomAuthenticationFlowCallback();
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Custom callback Factory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "callback";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return new AuthenticationExecutionModel.Requirement[]{AuthenticationExecutionModel.Requirement.REQUIRED};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Used for testing purposes of Callback factory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -23,4 +23,5 @@ org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory
|
|||
org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory
|
||||
org.keycloak.testsuite.forms.UsernameOnlyAuthenticator
|
||||
org.keycloak.testsuite.authentication.ConditionalUserAttributeValueFactory
|
||||
org.keycloak.testsuite.authentication.SetUserAttributeAuthenticatorFactory
|
||||
org.keycloak.testsuite.authentication.SetUserAttributeAuthenticatorFactory
|
||||
org.keycloak.testsuite.authentication.CustomAuthenticationFlowCallbackFactory
|
|
@ -1648,10 +1648,14 @@ public class OAuthClient {
|
|||
}
|
||||
|
||||
public OAuthClient claims(ClaimsRepresentation claims) {
|
||||
try {
|
||||
this.claims = URLEncoder.encode(JsonSerialization.writeValueAsString(claims), "UTF-8");
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
if (claims == null) {
|
||||
this.claims = null;
|
||||
} else {
|
||||
try {
|
||||
this.claims = URLEncoder.encode(JsonSerialization.writeValueAsString(claims), "UTF-8");
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -228,6 +228,9 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
|||
addProviderInfo(result, "user-session-limits", "User session count limiter",
|
||||
"Configures how many concurrent sessions a single user is allowed to create for this realm and/or client");
|
||||
|
||||
addProviderInfo(result, "custom-callback-authenticator", "Custom callback Factory",
|
||||
"Used for testing purposes of Callback factory");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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.testsuite.forms;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.authentication.authenticators.access.AllowAccessAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.authentication.CustomAuthenticationFlowCallback;
|
||||
import org.keycloak.testsuite.authentication.CustomAuthenticationFlowCallbackFactory;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
@AuthServerContainerExclude(REMOTE)
|
||||
public class AuthenticationFlowCallbackProviderTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loaEssentialNonExisting() {
|
||||
setBrowserFlow();
|
||||
LevelOfAssuranceFlowTest.openLoginFormWithAcrClaim(oauth, true, "4");
|
||||
|
||||
loginPage.assertCurrent();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
errorPage.assertCurrent();
|
||||
assertThat(errorPage.getError(), is("Authentication requirements not fulfilled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void errorWithCustomProvider() {
|
||||
setBrowserFlow();
|
||||
LevelOfAssuranceFlowTest.openLoginFormWithAcrClaim(oauth, true, "1");
|
||||
|
||||
loginPage.assertCurrent();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
errorPage.assertCurrent();
|
||||
assertThat(errorPage.getError(), is(CustomAuthenticationFlowCallback.EXPECTED_ERROR_MESSAGE));
|
||||
}
|
||||
|
||||
protected void setBrowserFlow() {
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow("newFlow"));
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow("newFlow")
|
||||
.inForms(forms -> forms
|
||||
.clear()
|
||||
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.CONDITIONAL, subflow -> subflow
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID)
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
||||
config -> {
|
||||
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1");
|
||||
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "true");
|
||||
})
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, AllowAccessAuthenticatorFactory.PROVIDER_ID)
|
||||
)
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, CustomAuthenticationFlowCallbackFactory.PROVIDER_ID)
|
||||
)
|
||||
.defineAsBrowserFlow() // Activate this new flow
|
||||
);
|
||||
}
|
||||
}
|
|
@ -49,8 +49,10 @@ import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
|||
import org.keycloak.testsuite.pages.PasswordPage;
|
||||
import org.keycloak.testsuite.pages.PushTheButtonPage;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
|
||||
/**
|
||||
|
@ -179,7 +181,6 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
// doing step-up authentication to level 2
|
||||
openLoginFormWithAcrClaim(true, "gold");
|
||||
authenticateWithPassword();
|
||||
authenticateWithButton();
|
||||
assertLoggedInWithAcr("gold");
|
||||
// step-up to level 3 needs password authentication because level 2 is not stored in user session
|
||||
openLoginFormWithAcrClaim(true, "3");
|
||||
|
@ -188,11 +189,22 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
assertLoggedInWithAcr("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stepupToUnknownEssentialAcrFails() {
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
assertLoggedInWithAcr("silver");
|
||||
// step-up to unknown acr
|
||||
openLoginFormWithAcrClaim(true, "uranium");
|
||||
assertErrorPage("Invalid parameter: claims");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reauthenticationWithNoAcr() {
|
||||
openLoginFormWithAcrClaim(true, "silver");
|
||||
authenticateWithUsername();
|
||||
assertLoggedInWithAcr("silver");
|
||||
oauth.claims(null);
|
||||
oauth.openLoginForm();
|
||||
assertLoggedInWithAcr("0");
|
||||
}
|
||||
|
@ -215,6 +227,15 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
assertLoggedInWithAcr("0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void essentialClaimNotReachedFails() {
|
||||
openLoginFormWithAcrClaim(true, "4");
|
||||
authenticateWithUsername();
|
||||
authenticateWithPassword();
|
||||
authenticateWithButton();
|
||||
assertErrorPage("Authentication requirements not fulfilled");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionalClaimNotReachedSucceeds() {
|
||||
openLoginFormWithAcrClaim(false, "4");
|
||||
|
@ -226,6 +247,12 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
assertLoggedInWithAcr("gold");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void essentialUnknownClaimFails() {
|
||||
openLoginFormWithAcrClaim(true, "uranium");
|
||||
assertErrorPage("Invalid parameter: claims");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionalUnknownClaimSucceeds() {
|
||||
openLoginFormWithAcrClaim(false, "iron");
|
||||
|
@ -259,13 +286,17 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
assertLoggedInWithAcr("gold");
|
||||
}
|
||||
|
||||
private void openLoginFormWithAcrClaim(boolean essential, String... acrValues) {
|
||||
public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) {
|
||||
openLoginFormWithAcrClaim(oauth, essential, acrValues);
|
||||
}
|
||||
|
||||
public static void openLoginFormWithAcrClaim(OAuthClient oauth, boolean essential, String... acrValues) {
|
||||
ClaimsRepresentation.ClaimValue<String> acrClaim = new ClaimsRepresentation.ClaimValue<>();
|
||||
acrClaim.setEssential(essential);
|
||||
acrClaim.setValues(Arrays.asList(acrValues));
|
||||
|
||||
ClaimsRepresentation claims = new ClaimsRepresentation();
|
||||
claims.setIdTokenClaims(Collections.singletonMap("acr", acrClaim));
|
||||
claims.setIdTokenClaims(Collections.singletonMap(IDToken.ACR, acrClaim));
|
||||
|
||||
oauth.claims(claims);
|
||||
oauth.openLoginForm();
|
||||
|
@ -291,4 +322,9 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent);
|
||||
Assert.assertEquals(acr, idToken.getAcr());
|
||||
}
|
||||
|
||||
private void assertErrorPage(String expectedError) {
|
||||
Assert.assertThat(true, is(errorPage.isCurrent()));
|
||||
Assert.assertEquals(expectedError, errorPage.getError());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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.testsuite.util;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.authentication.AuthenticatorUtil;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
@AuthServerContainerExclude(REMOTE)
|
||||
public class AuthenticatorUtilTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void variousFactoryProviders() {
|
||||
testingClient.server().run(session -> {
|
||||
|
||||
RealmModel realm = session.realms().getRealm(TEST_REALM_NAME);
|
||||
assertThat(realm, notNullValue());
|
||||
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
assertThat(client, notNullValue());
|
||||
|
||||
AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm)
|
||||
.createAuthenticationSession(client);
|
||||
assertThat(authSession, notNullValue());
|
||||
|
||||
Set<String> callbacksFactories = AuthenticatorUtil.getAuthCallbacksFactoryIds(authSession);
|
||||
assertThat(callbacksFactories, notNullValue());
|
||||
assertThat(callbacksFactories, Matchers.empty());
|
||||
|
||||
AuthenticatorUtil.setAuthCallbacksFactoryIds(authSession, "factory1");
|
||||
callbacksFactories = AuthenticatorUtil.getAuthCallbacksFactoryIds(authSession);
|
||||
assertThat(callbacksFactories, notNullValue());
|
||||
assertThat(callbacksFactories.size(), is(1));
|
||||
|
||||
String note = authSession.getAuthNote(AuthenticatorUtil.CALLBACKS_FACTORY_IDS_NOTE);
|
||||
assertThat(note, notNullValue());
|
||||
assertThat(note, is("factory1"));
|
||||
|
||||
AuthenticatorUtil.setAuthCallbacksFactoryIds(authSession, "factory2");
|
||||
callbacksFactories = AuthenticatorUtil.getAuthCallbacksFactoryIds(authSession);
|
||||
assertThat(callbacksFactories, notNullValue());
|
||||
assertThat(callbacksFactories.size(), is(2));
|
||||
|
||||
note = authSession.getAuthNote(AuthenticatorUtil.CALLBACKS_FACTORY_IDS_NOTE);
|
||||
assertThat(note, notNullValue());
|
||||
assertThat(note, is("factory1" + Constants.CFG_DELIMITER + "factory2"));
|
||||
|
||||
AuthenticatorUtil.setAuthCallbacksFactoryIds(authSession, "factory1");
|
||||
callbacksFactories = AuthenticatorUtil.getAuthCallbacksFactoryIds(authSession);
|
||||
assertThat(callbacksFactories, notNullValue());
|
||||
assertThat(callbacksFactories.size(), is(2));
|
||||
|
||||
note = authSession.getAuthNote(AuthenticatorUtil.CALLBACKS_FACTORY_IDS_NOTE);
|
||||
assertThat(note, notNullValue());
|
||||
assertThat(note, is("factory1" + Constants.CFG_DELIMITER + "factory2"));
|
||||
|
||||
AuthenticatorUtil.setAuthCallbacksFactoryIds(authSession, "");
|
||||
callbacksFactories = AuthenticatorUtil.getAuthCallbacksFactoryIds(authSession);
|
||||
assertThat(callbacksFactories, notNullValue());
|
||||
assertThat(callbacksFactories.size(), is(2));
|
||||
|
||||
note = authSession.getAuthNote(AuthenticatorUtil.CALLBACKS_FACTORY_IDS_NOTE);
|
||||
assertThat(note, notNullValue());
|
||||
assertThat(note, is("factory1" + Constants.CFG_DELIMITER + "factory2"));
|
||||
|
||||
AuthenticatorUtil.setAuthCallbacksFactoryIds(authSession, null);
|
||||
callbacksFactories = AuthenticatorUtil.getAuthCallbacksFactoryIds(authSession);
|
||||
assertThat(callbacksFactories, notNullValue());
|
||||
assertThat(callbacksFactories.size(), is(2));
|
||||
|
||||
note = authSession.getAuthNote(AuthenticatorUtil.CALLBACKS_FACTORY_IDS_NOTE);
|
||||
assertThat(note, notNullValue());
|
||||
assertThat(note, is("factory1" + Constants.CFG_DELIMITER + "factory2"));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -369,6 +369,7 @@ alreadyLoggedIn=You are already logged in.
|
|||
differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please sign out first.
|
||||
brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid.
|
||||
proceedWithAction=» Click here to proceed
|
||||
acrNotFulfilled=Authentication requirements not fulfilled
|
||||
|
||||
requiredAction.CONFIGURE_TOTP=Configure OTP
|
||||
requiredAction.terms_and_conditions=Terms and Conditions
|
||||
|
|
Loading…
Reference in a new issue