Always order required actions by priority (regardless of context)

- AuthenticationManager#actionRequired: make sure that the highest prioritized required action is performed first, possibly before the currently requested required action
- AuthenticationManager#nextRequiredAction: make sure that the next action is requested via URL, also based on highest priority (-> requested URL will match actually performed action, unless required actions for the user are changed by a parallel operation)
- add tests to RequiredActionPriorityTest, add helper method for priority setup to ApiUtil (for easier and more robust setup than up-to-now)
- fix test WebAuthnRegisterAndLoginTest - which failed because WebAuthnRegisterFactory (prio 70) is now executed before WebAuthnPasswordlessRegisterFactory (prio 80)

Closes #16873

Signed-off-by: Daniel Fesenmeyer <daniel.fesenmeyer@bosch.com>
This commit is contained in:
Daniel Fesenmeyer 2024-04-18 21:32:34 +02:00 committed by Marek Posolda
parent ab376d9101
commit c08621fa63
13 changed files with 476 additions and 130 deletions

View file

@ -23,6 +23,7 @@ package org.keycloak.events;
public interface Details { public interface Details {
String PREF_PREVIOUS = "previous_"; String PREF_PREVIOUS = "previous_";
String PREF_UPDATED = "updated_"; String PREF_UPDATED = "updated_";
String FIELDS_TO_UPDATE = "fields_to_update";
String CUSTOM_REQUIRED_ACTION="custom_required_action"; String CUSTOM_REQUIRED_ACTION="custom_required_action";
String CONTEXT = "context"; String CONTEXT = "context";

View file

@ -91,6 +91,11 @@ public final class Constants {
public static final String KC_ACTION_PARAMETER = "kc_action_parameter"; public static final String KC_ACTION_PARAMETER = "kc_action_parameter";
public static final String KC_ACTION_STATUS = "kc_action_status"; public static final String KC_ACTION_STATUS = "kc_action_status";
public static final String KC_ACTION_EXECUTING = "kc_action_executing"; public static final String KC_ACTION_EXECUTING = "kc_action_executing";
/**
* Auth session attribute whether an AIA is enforced, which means it cannot be cancelled.
* <p>Example use case: the action behind the AIA is also defined on the user (for example, UPDATE_PASSWORD).</p>
*/
public static final String KC_ACTION_ENFORCED = "kc_action_enforced";
public static final int KC_ACTION_MAX_AGE = 300; public static final int KC_ACTION_MAX_AGE = 300;
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST"; public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";

View file

@ -106,9 +106,9 @@ public interface UserModel extends RoleMapperModel {
Map<String, List<String>> getAttributes(); Map<String, List<String>> getAttributes();
/** /**
* Obtains the names of required actions associated with the user. * Obtains the aliases of required actions associated with the user.
* *
* @return a non-null {@link Stream} of required action names. * @return a non-null {@link Stream} of required action aliases.
*/ */
Stream<String> getRequiredActionsStream(); Stream<String> getRequiredActionsStream();

View file

@ -74,7 +74,7 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
void setAuthenticatedUser(UserModel user); void setAuthenticatedUser(UserModel user);
/** /**
* Returns required actions that are attached to this client session. * Returns required actions (aliases) that are attached to this client session.
* @return {@code Set<String>} Never returns {@code null}. * @return {@code Set<String>} Never returns {@code null}.
*/ */
Set<String> getRequiredActions(); Set<String> getRequiredActions();

View file

@ -27,6 +27,7 @@ import jakarta.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -90,7 +91,7 @@ public class VerifyUserProfile extends UpdateProfile {
EventBuilder event = context.getEvent().clone(); EventBuilder event = context.getEvent().clone();
event.event(EventType.VERIFY_PROFILE); event.event(EventType.VERIFY_PROFILE);
event.detail("fields_to_update", collectFields(errors)); event.detail(Details.FIELDS_TO_UPDATE, collectFields(errors));
event.success(); event.success();
} }
} }

View file

@ -543,7 +543,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session)); attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));
} }
if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null) { if (authenticationSession != null && authenticationSession.getClientNote(Constants.KC_ACTION_EXECUTING) != null
&& !Boolean.TRUE.toString().equals(authenticationSession.getClientNote(Constants.KC_ACTION_ENFORCED))) {
attributes.put("isAppInitiatedAction", true); attributes.put("isAppInitiatedAction", true);
} }
} }

View file

@ -102,6 +102,8 @@ import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -109,6 +111,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -1032,45 +1035,40 @@ public class AuthenticationManager {
return redirectAfterSuccessfulFlow(session, realm, userSession, clientSessionCtx, request, uriInfo, clientConnection, event, authSession); return redirectAfterSuccessfulFlow(session, realm, userSession, clientSessionCtx, request, uriInfo, clientConnection, event, authSession);
} }
// Return null if action is not required. Or the name of the requiredAction in case it is required. // Return null if action is not required. Or the alias of the requiredAction in case it is required.
public static String nextRequiredAction(final KeycloakSession session, final AuthenticationSessionModel authSession, public static String nextRequiredAction(final KeycloakSession session, final AuthenticationSessionModel authSession,
final HttpRequest request, final EventBuilder event) { final HttpRequest request, final EventBuilder event) {
final RealmModel realm = authSession.getRealm(); final var realm = authSession.getRealm();
final UserModel user = authSession.getAuthenticatedUser(); final var user = authSession.getAuthenticatedUser();
final ClientModel client = authSession.getClient();
evaluateRequiredActionTriggers(session, authSession, request, event, realm, user); evaluateRequiredActionTriggers(session, authSession, request, event, realm, user);
Optional<String> reqAction = user.getRequiredActionsStream().findFirst(); final var kcAction = authSession.getClientNote(Constants.KC_ACTION);
if (reqAction.isPresent()) { final var nextApplicableAction =
return reqAction.get(); getFirstApplicableRequiredAction(realm, authSession, user, kcAction);
} if (nextApplicableAction != null) {
if (!authSession.getRequiredActions().isEmpty()) { return nextApplicableAction.getAlias();
return authSession.getRequiredActions().iterator().next();
}
String kcAction = authSession.getClientNote(Constants.KC_ACTION);
if (kcAction != null) {
return kcAction;
} }
final var client = authSession.getClient();
if (client.isConsentRequired() || isOAuth2DeviceVerificationFlow(authSession)) { if (client.isConsentRequired() || isOAuth2DeviceVerificationFlow(authSession)) {
UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession); UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession);
// See if any clientScopes need to be approved on consent screen // See if any clientScopes need to be approved on consent screen
List<AuthorizationDetails> clientScopesToApprove = getClientScopesToApproveOnConsentScreen(grantedConsent, session, authSession); List<AuthorizationDetails> clientScopesToApprove =
getClientScopesToApproveOnConsentScreen(grantedConsent, session, authSession);
if (!clientScopesToApprove.isEmpty()) { if (!clientScopesToApprove.isEmpty()) {
return CommonClientSessionModel.Action.OAUTH_GRANT.name(); return CommonClientSessionModel.Action.OAUTH_GRANT.name();
} }
String consentDetail = (grantedConsent != null) ? Details.CONSENT_VALUE_PERSISTED_CONSENT : Details.CONSENT_VALUE_NO_CONSENT_REQUIRED; String consentDetail = (grantedConsent != null) ? Details.CONSENT_VALUE_PERSISTED_CONSENT
: Details.CONSENT_VALUE_NO_CONSENT_REQUIRED;
event.detail(Details.CONSENT, consentDetail); event.detail(Details.CONSENT, consentDetail);
} else { } else {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED); event.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED);
} }
return null; return null;
} }
@ -1098,23 +1096,20 @@ public class AuthenticationManager {
public static Response actionRequired(final KeycloakSession session, final AuthenticationSessionModel authSession, public static Response actionRequired(final KeycloakSession session, final AuthenticationSessionModel authSession,
final HttpRequest request, final EventBuilder event) { final HttpRequest request, final EventBuilder event) {
final RealmModel realm = authSession.getRealm(); final var realm = authSession.getRealm();
final UserModel user = authSession.getAuthenticatedUser(); final var user = authSession.getAuthenticatedUser();
final ClientModel client = authSession.getClient();
evaluateRequiredActionTriggers(session, authSession, request, event, realm, user); evaluateRequiredActionTriggers(session, authSession, request, event, realm, user);
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
event.detail(Details.CODE_ID, authSession.getParentSession().getId()); event.detail(Details.CODE_ID, authSession.getParentSession().getId());
Stream<String> requiredActions = user.getRequiredActionsStream(); final var actionResponse = executionActions(session, authSession, request, event, realm, user);
Response action = executionActions(session, authSession, request, event, realm, user, requiredActions); if (actionResponse != null) {
if (action != null) return action; return actionResponse;
}
// executionActions() method should remove any duplicate actions that might be in the clientSession final var client = authSession.getClient();
action = executionActions(session, authSession, request, event, realm, user, authSession.getRequiredActions().stream()); logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
if (action != null) return action;
// https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-5.4 // https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-5.4
// The spec says "The authorization server SHOULD display information about the device", // The spec says "The authorization server SHOULD display information about the device",
@ -1123,13 +1118,15 @@ public class AuthenticationManager {
UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession); UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession);
List<AuthorizationDetails> clientScopesToApprove = getClientScopesToApproveOnConsentScreen(grantedConsent, session, authSession); List<AuthorizationDetails> clientScopesToApprove =
getClientScopesToApproveOnConsentScreen(grantedConsent, session, authSession);
// Skip grant screen if everything was already approved by this user // Skip grant screen if everything was already approved by this user
if (clientScopesToApprove.size() > 0) { if (clientScopesToApprove.size() > 0) {
String execution = AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name(); String execution = AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name();
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession); ClientSessionCode<AuthenticationSessionModel> accessCode =
new ClientSessionCode<>(session, realm, authSession);
accessCode.setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name()); accessCode.setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name());
authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution); authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution);
@ -1140,7 +1137,8 @@ public class AuthenticationManager {
.setAccessRequest(clientScopesToApprove) .setAccessRequest(clientScopesToApprove)
.createOAuthGrant(); .createOAuthGrant();
} else { } else {
String consentDetail = (grantedConsent != null) ? Details.CONSENT_VALUE_PERSISTED_CONSENT : Details.CONSENT_VALUE_NO_CONSENT_REQUIRED; String consentDetail = (grantedConsent != null) ? Details.CONSENT_VALUE_PERSISTED_CONSENT
: Details.CONSENT_VALUE_NO_CONSENT_REQUIRED;
event.detail(Details.CONSENT, consentDetail); event.detail(Details.CONSENT, consentDetail);
} }
} else { } else {
@ -1220,26 +1218,14 @@ public class AuthenticationManager {
protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession, protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user, HttpRequest request, EventBuilder event, RealmModel realm, UserModel user) {
Stream<String> requiredActions) { final var kcAction = authSession.getClientNote(Constants.KC_ACTION);
final var firstApplicableRequiredAction =
getFirstApplicableRequiredAction(realm, authSession, user, kcAction);
Optional<Response> response = sortRequiredActionsByPriority(realm, requiredActions) if (firstApplicableRequiredAction != null) {
.map(model -> executeAction(session, authSession, model, request, event, realm, user, false)) return executeAction(session, authSession, firstApplicableRequiredAction, request, event, realm, user,
.filter(Objects::nonNull).findFirst(); kcAction != null);
if (response.isPresent())
return response.get();
String kcAction = authSession.getClientNote(Constants.KC_ACTION);
if (kcAction != null) {
Optional<RequiredActionProviderModel> requiredAction = realm.getRequiredActionProvidersStream()
.filter(m -> Objects.equals(m.getProviderId(), kcAction))
.findFirst();
if (requiredAction.isPresent()) {
return executeAction(session, authSession, requiredAction.get(), request, event, realm, user, true);
}
logger.debugv("Requested action {0} not configured for realm", kcAction);
setKcActionStatus(kcAction, RequiredActionContext.KcActionStatus.ERROR, authSession);
} }
return null; return null;
@ -1304,17 +1290,81 @@ public class AuthenticationManager {
return null; return null;
} }
private static Stream<RequiredActionProviderModel> sortRequiredActionsByPriority(RealmModel realm, Stream<String> requiredActions) { private static RequiredActionProviderModel getFirstApplicableRequiredAction(final RealmModel realm,
return requiredActions.map(action -> { final AuthenticationSessionModel authSession, final UserModel user, final String kcAction) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action); final var applicableRequiredActionsSorted =
if (model == null) { getApplicableRequiredActionsSorted(realm, authSession, user, kcAction);
logger.warnv("Could not find configuration for Required Action {0}, did you forget to register it?", action);
final RequiredActionProviderModel firstApplicableRequiredAction;
if (applicableRequiredActionsSorted.isEmpty()) {
firstApplicableRequiredAction = null;
logger.debugv("Did not find applicable required action");
} else {
firstApplicableRequiredAction = applicableRequiredActionsSorted.iterator().next();
logger.debugv("first applicable required action: {0}", firstApplicableRequiredAction.getAlias());
} }
return model;
}) return firstApplicableRequiredAction;
}
private static List<RequiredActionProviderModel> getApplicableRequiredActionsSorted(final RealmModel realm,
final AuthenticationSessionModel authSession, final UserModel user, final String kcActionAlias) {
final Set<String> nonInitiatedActionAliases = new HashSet<>();
nonInitiatedActionAliases.addAll(user.getRequiredActionsStream().toList());
nonInitiatedActionAliases.addAll(authSession.getRequiredActions());
final var applicableNonInitiatedActions = nonInitiatedActionAliases.stream()
.map(alias -> getApplicableRequiredAction(realm, alias))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.filter(RequiredActionProviderModel::isEnabled) .collect(Collectors.toMap(RequiredActionProviderModel::getAlias, Function.identity()));
.sorted(RequiredActionProviderModel.RequiredActionComparator.SINGLETON);
RequiredActionProviderModel kcAction = null;
if (kcActionAlias != null) {
kcAction = getApplicableRequiredAction(realm, kcActionAlias);
if (kcAction == null) {
logger.debugv("Requested action {0} not configured for realm", kcActionAlias);
setKcActionStatus(kcActionAlias, RequiredActionContext.KcActionStatus.ERROR, authSession);
} else {
if (applicableNonInitiatedActions.containsKey(kcActionAlias)) {
setKcActionToEnforced(kcActionAlias, authSession);
}
}
}
final Map<String, RequiredActionProviderModel> applicableActions;
if (kcAction != null) {
applicableActions = new HashMap<>(applicableNonInitiatedActions);
applicableActions.put(kcAction.getAlias(), kcAction);
} else {
applicableActions = applicableNonInitiatedActions;
}
final var applicableActionsSorted = applicableActions.values().stream()
.sorted(RequiredActionProviderModel.RequiredActionComparator.SINGLETON)
.toList();
if (logger.isDebugEnabled()) {
logger.debugv("applicable required actions (sorted): {0}",
applicableActionsSorted.stream().map(RequiredActionProviderModel::getAlias).toList());
}
return applicableActionsSorted;
}
private static RequiredActionProviderModel getApplicableRequiredAction(final RealmModel realm, final String alias) {
final var model = realm.getRequiredActionProviderByAlias(alias);
if (model == null) {
logger.warnv(
"Could not find configuration for Required Action {0}, did you forget to register it?",
alias);
return null;
}
if (!model.isEnabled()) {
return null;
}
return model;
} }
public static void evaluateRequiredActionTriggers(final KeycloakSession session, final AuthenticationSessionModel authSession, public static void evaluateRequiredActionTriggers(final KeycloakSession session, final AuthenticationSessionModel authSession,
@ -1524,6 +1574,12 @@ public class AuthenticationManager {
} }
} }
public static void setKcActionToEnforced(String executedProviderId, AuthenticationSessionModel authSession) {
if (executedProviderId.equals(authSession.getClientNote(Constants.KC_ACTION))) {
authSession.setClientNote(Constants.KC_ACTION_ENFORCED, Boolean.TRUE.toString());
}
}
public static void logSuccess(KeycloakSession session, AuthenticationSessionModel authSession) { public static void logSuccess(KeycloakSession session, AuthenticationSessionModel authSession) {
RealmModel realm = session.getContext().getRealm(); RealmModel realm = session.getContext().getRealm();
if (realm.isBruteForceProtected()) { if (realm.isBruteForceProtected()) {

View file

@ -1190,7 +1190,8 @@ public class LoginActionsService {
} }
private boolean isCancelAppInitiatedAction(String providerId, AuthenticationSessionModel authSession, RequiredActionContextResult context) { private boolean isCancelAppInitiatedAction(String providerId, AuthenticationSessionModel authSession, RequiredActionContextResult context) {
if (providerId.equals(authSession.getClientNote(Constants.KC_ACTION_EXECUTING))) { if (providerId.equals(authSession.getClientNote(Constants.KC_ACTION_EXECUTING))
&& !Boolean.TRUE.toString().equals(authSession.getClientNote(Constants.KC_ACTION_ENFORCED))) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
boolean userRequestedCancelAIA = formData.getFirst(CANCEL_AIA) != null; boolean userRequestedCancelAIA = formData.getFirst(CANCEL_AIA) != null;
return userRequestedCancelAIA; return userRequestedCancelAIA;

View file

@ -24,11 +24,13 @@ import org.keycloak.admin.client.resource.GroupResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
@ -40,6 +42,7 @@ import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD;
@ -163,7 +166,6 @@ public class ApiUtil {
* Creates a user * Creates a user
* @param realm * @param realm
* @param user * @param user
* @param password
* @return ID of the new user * @return ID of the new user
*/ */
public static String createUserWithAdminClient(RealmResource realm, UserRepresentation user) { public static String createUserWithAdminClient(RealmResource realm, UserRepresentation user) {
@ -276,4 +278,62 @@ public class ApiUtil {
return null; return null;
} }
/**
* Updates the order of required actions
*
* @param realmResource the realm
* @param requiredActionsInTargetOrder the required actions for which the order should be changed (order will be the
* order of this list) - can be a subset of the available required actions
* @see #updateRequiredActionsOrderByAlias(RealmResource, List)
*/
public static void updateRequiredActionsOrder(final RealmResource realmResource,
final List<UserModel.RequiredAction> requiredActionsInTargetOrder) {
updateRequiredActionsOrderByAlias(realmResource,
requiredActionsInTargetOrder.stream().map(Enum::name).collect(Collectors.toList()));
}
/**
* @see #updateRequiredActionsOrder(RealmResource, List)
*/
public static void updateRequiredActionsOrderByAlias(final RealmResource realmResource,
final List<String> requiredActionsInTargetOrder) {
final var realmName = realmResource.toRepresentation().getRealm();
final var initialRequiredActionsOrdered = realmResource.flows().getRequiredActions().stream()
.map(RequiredActionProviderRepresentation::getAlias).collect(Collectors.toList());
log.infof("initial required actions order for realm '%s': %s", realmName, initialRequiredActionsOrdered);
log.infof("target order for realm '%s' (maybe partial): %s", realmName, requiredActionsInTargetOrder);
final var requiredActionsToConfigureWithLowerPrio = new ArrayList<>(requiredActionsInTargetOrder);
for (final var requiredActionAlias : requiredActionsInTargetOrder) {
var allRequiredActionsOrdered = realmResource.flows().getRequiredActions().stream()
.map(RequiredActionProviderRepresentation::getAlias).collect(Collectors.toList());
requiredActionsToConfigureWithLowerPrio.remove(requiredActionAlias);
final var currentIndex = allRequiredActionsOrdered.indexOf(requiredActionAlias);
if (currentIndex == -1) {
throw new IllegalStateException("Required action not found: " + requiredActionAlias);
}
final var aliasOfCurrentlyFirstActionWithLowerTargetPrioOpt = allRequiredActionsOrdered.stream()
.filter(requiredActionsToConfigureWithLowerPrio::contains).findFirst();
aliasOfCurrentlyFirstActionWithLowerTargetPrioOpt
.ifPresent(aliasOfCurrentlyFirstActionWithLowerTargetPrio -> {
final var indexOfCurrentlyFirstActionWithLowerTargetPrio =
allRequiredActionsOrdered.indexOf(aliasOfCurrentlyFirstActionWithLowerTargetPrio);
final var positionsToMoveCurrentActionUp =
Math.max(currentIndex - indexOfCurrentlyFirstActionWithLowerTargetPrio, 0);
if (positionsToMoveCurrentActionUp > 0) {
for (var i = 0; i < positionsToMoveCurrentActionUp; i++) {
realmResource.flows().raiseRequiredActionPriority(requiredActionAlias);
}
}
});
}
final var updatedRequiredActionsOrdered = realmResource.flows().getRequiredActions().stream()
.map(RequiredActionProviderRepresentation::getAlias).collect(Collectors.toList());
log.infof("updated required actions order for realm '%s': %s", realmName, updatedRequiredActionsOrdered);
}
} }

View file

@ -204,6 +204,10 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
doAIA(); doAIA();
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
/*
* cancel should not be supported, because the action is not only application-initiated, but also required by
* Keycloak
*/
assertFalse(changePasswordPage.isCancelDisplayed()); assertFalse(changePasswordPage.isCancelDisplayed());
changePasswordPage.changePassword("new-password", "new-password"); changePasswordPage.changePassword("new-password", "new-password");

View file

@ -20,9 +20,11 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest.getEmailLink;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -33,7 +35,6 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
@ -41,34 +42,51 @@ import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginConfigTotpPage; import org.keycloak.testsuite.pages.LoginConfigTotpPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.pages.TermsAndConditionsPage; import org.keycloak.testsuite.pages.TermsAndConditionsPage;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.pages.VerifyProfilePage;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.OAuthClient;
import java.util.List;
/** /**
* @author <a href="mailto:wadahiro@gmail.com">Hiroyuki Wada</a> * @author <a href="mailto:wadahiro@gmail.com">Hiroyuki Wada</a>
*/ */
public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest { public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest {
@Override private static final String EMAIL = "test-user@localhost";
public void configureTestRealm(RealmRepresentation testRealm) { private static final String USERNAME = EMAIL;
} private static final String PASSWORD = "password";
private static final String NEW_EMAIL = "new@email.com";
private static final String NEW_FIRST_NAME = "New first";
private static final String NEW_LAST_NAME = "New last";
private static final String NEW_PASSWORD = "new-password";
@Rule @Rule
public AssertEvents events = new AssertEvents(this); public AssertEvents events = new AssertEvents(this);
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@Page @Page
protected AppPage appPage; protected AppPage appPage;
@Page @Page
protected LoginPage loginPage; protected LoginPage loginPage;
@Page
protected LoginPasswordResetPage resetPasswordPage;
@Page @Page
protected LoginPasswordUpdatePage changePasswordPage; protected LoginPasswordUpdatePage changePasswordPage;
@Page @Page
protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage; protected LoginUpdateProfilePage updateProfilePage;
@Page
protected VerifyProfilePage verifyProfilePage;
@Page @Page
protected TermsAndConditionsPage termsPage; protected TermsAndConditionsPage termsPage;
@ -76,29 +94,41 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest {
@Page @Page
protected LoginConfigTotpPage totpPage; protected LoginConfigTotpPage totpPage;
private String testUserId;
@Before @Before
public void setupRequiredActions() { public void beforeEach() {
setRequiredActionEnabled("test", TermsAndConditions.PROVIDER_ID, true, false); setRequiredActionEnabled(TEST_REALM_NAME, TermsAndConditions.PROVIDER_ID, true, false);
// Because of changing the password in test case, we need to re-create the user. testUserId = ApiUtil.findUserByUsernameId(testRealm(), USERNAME).toRepresentation().getId();
ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost"); }
UserRepresentation user = UserBuilder.create().enabled(true).username("test-user@localhost")
.email("test-user@localhost").build();
String testUserId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
setRequiredActionEnabled("test", testUserId, RequiredAction.UPDATE_PASSWORD.name(), true); @Override
setRequiredActionEnabled("test", testUserId, RequiredAction.UPDATE_PROFILE.name(), true); public void configureTestRealm(final RealmRepresentation testRealm) {
setRequiredActionEnabled("test", testUserId, TermsAndConditions.PROVIDER_ID, true); testRealm.setResetPasswordAllowed(true);
}
@Override
protected boolean isImportAfterEachMethod() {
return true;
}
@Override
protected boolean removeVerifyProfileAtImport() {
return false;
} }
@Test @Test
public void executeRequiredActionsWithDefaultPriority() throws Exception { public void executeRequiredActionsWithDefaultPriority() {
// Default priority is alphabetical order: // Default priority is alphabetical order:
// TermsAndConditions -> UpdatePassword -> UpdateProfile // TermsAndConditions -> UpdatePassword -> UpdateProfile
enableRequiredActionForUser(RequiredAction.UPDATE_PASSWORD);
enableRequiredActionForUser(RequiredAction.UPDATE_PROFILE);
enableRequiredActionForUser(RequiredAction.TERMS_AND_CONDITIONS);
// Login // Login
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login(USERNAME, PASSWORD);
// First, accept terms // First, accept terms
termsPage.assertCurrent(); termsPage.assertCurrent();
@ -108,50 +138,60 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest {
// Second, change password // Second, change password
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password"); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD);
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
// Finally, update profile // Finally, update profile
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); updateProfilePage.prepareUpdate().firstName(NEW_FIRST_NAME).lastName(NEW_LAST_NAME)
events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, "New first") .email(NEW_EMAIL).submit();
.detail(Details.UPDATED_LAST_NAME, "New last") events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, NEW_FIRST_NAME)
.detail(Details.PREVIOUS_EMAIL, "test-user@localhost") .detail(Details.UPDATED_LAST_NAME, NEW_LAST_NAME)
.detail(Details.UPDATED_EMAIL, "new@email.com") .detail(Details.PREVIOUS_EMAIL, EMAIL)
.detail(Details.UPDATED_EMAIL, NEW_EMAIL)
.assertEvent(); .assertEvent();
// Logged in // Logged in
appPage.assertCurrent(); appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
} }
@Test @Test
public void executeRequiredActionsWithCustomPriority() throws Exception { public void executeRequiredActionsWithCustomPriority() {
// Default priority is alphabetical order: // Default priority is alphabetical order:
// TermsAndConditions -> UpdatePassword -> UpdateProfile // TermsAndConditions -> UpdatePassword -> UpdateProfile
// After Changing the priority, the order will be: // After Changing the priority, the order will be:
// UpdatePassword -> UpdateProfile -> TermsAndConditions // UpdatePassword -> UpdateProfile -> TermsAndConditions
testRealm().flows().raiseRequiredActionPriority(UserModel.RequiredAction.UPDATE_PASSWORD.name()); final var requiredActionsCustomOrdered = List.of(
testRealm().flows().lowerRequiredActionPriority(UserModel.RequiredAction.TERMS_AND_CONDITIONS.name()); RequiredAction.UPDATE_PASSWORD,
RequiredAction.UPDATE_PROFILE,
RequiredAction.TERMS_AND_CONDITIONS
);
ApiUtil.updateRequiredActionsOrder(testRealm(), requiredActionsCustomOrdered);
enableRequiredActionForUser(RequiredAction.UPDATE_PASSWORD);
enableRequiredActionForUser(RequiredAction.UPDATE_PROFILE);
enableRequiredActionForUser(RequiredAction.TERMS_AND_CONDITIONS);
// Login // Login
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login(USERNAME, PASSWORD);
// First, change password // First, change password
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password"); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD);
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
// Second, update profile // Second, update profile
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last").email("new@email.com").submit(); updateProfilePage.prepareUpdate().firstName(NEW_FIRST_NAME).lastName(NEW_LAST_NAME)
events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, "New first") .email(NEW_EMAIL).submit();
.detail(Details.UPDATED_LAST_NAME, "New last") events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, NEW_FIRST_NAME)
.detail(Details.PREVIOUS_EMAIL, "test-user@localhost") .detail(Details.UPDATED_LAST_NAME, NEW_LAST_NAME)
.detail(Details.UPDATED_EMAIL, "new@email.com") .detail(Details.PREVIOUS_EMAIL, EMAIL)
.detail(Details.UPDATED_EMAIL, NEW_EMAIL)
.assertEvent(); .assertEvent();
// Finally, accept terms // Finally, accept terms
@ -160,32 +200,201 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest {
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI) events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI)
.detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent(); .detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent();
// Logined // Logged in
appPage.assertCurrent(); appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
public void executeRequiredActionsWithCustomPriorityAppliesSamePriorityToUserActionsAndKcActionParam() {
// Default priority is alphabetical order:
// TermsAndConditions -> UpdatePassword -> UpdateProfile
// After Changing the priority, the order will be:
// UpdatePassword -> UpdateProfile -> TermsAndConditions
final var requiredActionsCustomOrdered = List.of(
RequiredAction.UPDATE_PASSWORD,
RequiredAction.UPDATE_PROFILE,
RequiredAction.TERMS_AND_CONDITIONS
);
ApiUtil.updateRequiredActionsOrder(testRealm(), requiredActionsCustomOrdered);
enableRequiredActionForUser(RequiredAction.UPDATE_PASSWORD);
// we don't enable UPDATE_PROFILE for the user, we set this as kc_action param instead
enableRequiredActionForUser(RequiredAction.TERMS_AND_CONDITIONS);
// Login with kc_action=UPDATE_PROFILE
final var kcActionOauth = new OAuthClient();
kcActionOauth.init(driver);
kcActionOauth.kcAction(RequiredAction.UPDATE_PROFILE.name());
kcActionOauth.openLoginForm();
loginPage.assertCurrent(TEST_REALM_NAME);
loginPage.login(USERNAME, PASSWORD);
// First, change password
changePasswordPage.assertCurrent();
changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD);
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
// Second, update profile
updateProfilePage.assertCurrent();
updateProfilePage.prepareUpdate().firstName(NEW_FIRST_NAME).lastName(NEW_LAST_NAME)
.email(NEW_EMAIL).submit();
events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, NEW_FIRST_NAME)
.detail(Details.UPDATED_LAST_NAME, NEW_LAST_NAME)
.detail(Details.PREVIOUS_EMAIL, EMAIL)
.detail(Details.UPDATED_EMAIL, NEW_EMAIL)
.assertEvent();
// Finally, accept terms
termsPage.assertCurrent();
termsPage.acceptTerms();
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI)
.detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent();
// Logged in
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
public void executeLoginActionWithCustomPriorityAppliesSamePriorityToSessionAndUserActions()
throws Exception {
// Default priority is alphabetical order:
// TermsAndConditions -> UpdatePassword -> UpdateProfile
// After Changing the priority, the order will be:
// UpdatePassword -> UpdateProfile -> TermsAndConditions
final var requiredActionsCustomOrdered = List.of(
RequiredAction.UPDATE_PASSWORD,
RequiredAction.UPDATE_PROFILE,
RequiredAction.TERMS_AND_CONDITIONS
);
ApiUtil.updateRequiredActionsOrder(testRealm(), requiredActionsCustomOrdered);
// NOTE: we don't configure UPDATE_PASSWORD on the user - it's set on the session by the reset-password flow
enableRequiredActionForUser(RequiredAction.UPDATE_PROFILE);
enableRequiredActionForUser(RequiredAction.TERMS_AND_CONDITIONS);
// Get a password reset link
loginPage.open();
loginPage.assertCurrent(TEST_REALM_NAME);
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword(USERNAME);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).assertEvent();
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
assertEquals(1, greenMail.getReceivedMessages().length);
final var message = greenMail.getLastReceivedMessage();
final var resetUrl = getEmailLink(message);
assertNotNull(resetUrl);
driver.navigate().to(resetUrl);
// First, change password
changePasswordPage.assertCurrent();
changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD);
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
// Second, update profile
updateProfilePage.assertCurrent();
updateProfilePage.prepareUpdate().firstName(NEW_FIRST_NAME).lastName(NEW_LAST_NAME)
.email(NEW_EMAIL).submit();
events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.UPDATED_FIRST_NAME, NEW_FIRST_NAME)
.detail(Details.UPDATED_LAST_NAME, NEW_LAST_NAME)
.detail(Details.PREVIOUS_EMAIL, EMAIL)
.detail(Details.UPDATED_EMAIL, NEW_EMAIL)
.assertEvent();
// Finally, accept terms
termsPage.assertCurrent();
termsPage.acceptTerms();
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI)
.detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent();
// Logged in
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
@Test
public void executeRequiredActionWithCustomPriorityAppliesSamePriorityToSessionAndUserActions() {
// Default priority is alphabetical order:
// TermsAndConditions -> VerifyProfile
// After Changing the priority, the order will be:
// VerifyProfile -> TermsAndConditions
final var requiredActionsCustomOrdered = List.of(
RequiredAction.VERIFY_PROFILE,
RequiredAction.TERMS_AND_CONDITIONS
);
ApiUtil.updateRequiredActionsOrder(testRealm(), requiredActionsCustomOrdered);
// make user profile invalid by setting lastName to empty
final var userResource = testRealm().users().get(testUserId);
final var user = userResource.toRepresentation();
user.setLastName("");
userResource.update(user);
/* NOTE: we don't configure VERIFY_PROFILE on the user - it's set on the session because the profile is incomplete */
enableRequiredActionForUser(RequiredAction.TERMS_AND_CONDITIONS);
// Get a password reset link
loginPage.open();
loginPage.assertCurrent(TEST_REALM_NAME);
loginPage.login(USERNAME, PASSWORD);
// Second, complete the profile
verifyProfilePage.assertCurrent();
events.expectRequiredAction(EventType.VERIFY_PROFILE)
.user(testUserId)
.detail(Details.FIELDS_TO_UPDATE, UserModel.LAST_NAME)
.assertEvent();
verifyProfilePage.update(NEW_FIRST_NAME, NEW_LAST_NAME);
events.expectRequiredAction(EventType.UPDATE_PROFILE)
.user(testUserId)
.detail(Details.UPDATED_FIRST_NAME, NEW_FIRST_NAME)
.detail(Details.UPDATED_LAST_NAME, NEW_LAST_NAME)
.assertEvent();
// Finally, accept terms
termsPage.assertCurrent();
termsPage.acceptTerms();
events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI)
.detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent();
// Logged in
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
} }
@Test @Test
public void setupTotpAfterUpdatePassword() { public void setupTotpAfterUpdatePassword() {
String testUserId = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost").getId(); enableRequiredActionForUser(RequiredAction.CONFIGURE_TOTP);
enableRequiredActionForUser(RequiredAction.UPDATE_PASSWORD);
setRequiredActionEnabled("test", testUserId, RequiredAction.CONFIGURE_TOTP.name(), true); // move UPDATE_PASSWORD before top
setRequiredActionEnabled("test", testUserId, RequiredAction.UPDATE_PASSWORD.name(), true); final var requiredActionsCustomOrdered = List.of(
setRequiredActionEnabled("test", testUserId, TermsAndConditions.PROVIDER_ID, false); RequiredAction.UPDATE_PASSWORD,
setRequiredActionEnabled("test", testUserId, RequiredAction.UPDATE_PROFILE.name(), false); RequiredAction.CONFIGURE_TOTP
);
// make UPDATE_PASSWORD on top ApiUtil.updateRequiredActionsOrder(testRealm(), requiredActionsCustomOrdered);
testRealm().flows().raiseRequiredActionPriority(UserModel.RequiredAction.UPDATE_PASSWORD.name());
testRealm().flows().raiseRequiredActionPriority(UserModel.RequiredAction.UPDATE_PASSWORD.name());
// Login // Login
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login(USERNAME, PASSWORD);
// change password // change password
changePasswordPage.assertCurrent(); changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password"); changePasswordPage.changePassword(NEW_PASSWORD, NEW_PASSWORD);
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
// CONFIGURE_TOTP // CONFIGURE_TOTP
@ -200,10 +409,15 @@ public class RequiredActionPriorityTest extends AbstractTestRealmKeycloakTest {
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()), "userLabel"); totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()), "userLabel");
events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent(); events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent();
// Logined // Logged in
appPage.assertCurrent(); appPage.assertCurrent();
assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE)); assertThat(appPage.getRequestType(), is(RequestType.AUTH_RESPONSE));
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
} }
private void enableRequiredActionForUser(final RequiredAction requiredAction) {
setRequiredActionEnabled(TEST_REALM_NAME, testUserId, requiredAction.name(), true);
}
} }

View file

@ -350,7 +350,9 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
verifyProfilePage.assertCurrent(); verifyProfilePage.assertCurrent();
//event when form is shown //event when form is shown
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).detail("fields_to_update", "department").assertEvent(); events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id)
.detail(Details.FIELDS_TO_UPDATE, "department")
.assertEvent();
verifyProfilePage.update("First", "Last", "Department"); verifyProfilePage.update("First", "Last", "Department");
//event after profile is updated //event after profile is updated

View file

@ -227,11 +227,18 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
webAuthnRegisterPage.assertCurrent(); webAuthnRegisterPage.assertCurrent();
webAuthnRegisterPage.clickRegister(); webAuthnRegisterPage.clickRegister();
webAuthnRegisterPage.registerWebAuthnCredential(PASSWORDLESS_LABEL); webAuthnRegisterPage.registerWebAuthnCredential(WEBAUTHN_LABEL);
webAuthnRegisterPage.assertCurrent(); webAuthnRegisterPage.assertCurrent();
events.expectRequiredAction(CUSTOM_REQUIRED_ACTION)
.user(userId)
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL)
.assertEvent();
webAuthnRegisterPage.clickRegister(); webAuthnRegisterPage.clickRegister();
webAuthnRegisterPage.registerWebAuthnCredential(WEBAUTHN_LABEL); webAuthnRegisterPage.registerWebAuthnCredential(PASSWORDLESS_LABEL);
appPage.assertCurrent(); appPage.assertCurrent();
@ -241,12 +248,6 @@ public class WebAuthnRegisterAndLoginTest extends AbstractWebAuthnVirtualTest {
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL) .detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, PASSWORDLESS_LABEL)
.assertEvent(); .assertEvent();
events.expectRequiredAction(CUSTOM_REQUIRED_ACTION)
.user(userId)
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, WEBAUTHN_LABEL)
.assertEvent();
final String sessionID = events.expectLogin() final String sessionID = events.expectLogin()
.user(userId) .user(userId)
.assertEvent() .assertEvent()