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:
parent
ab376d9101
commit
c08621fa63
13 changed files with 476 additions and 130 deletions
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1097,24 +1095,21 @@ 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;
|
||||||
return model;
|
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 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()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue