8000 Always order required actions by priority (regardless of context) by danielFesenmeyer · Pull Request #28956 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8000
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
public interface Details {
String PREF_PREVIOUS = "previous_";
String PREF_UPDATED = "updated_";
String FIELDS_TO_UPDATE = "fields_to_update";

String CUSTOM_REQUIRED_ACTION="custom_required_action";
String CONTEXT = "context";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ public final class Constants {
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_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 String IS_AIA_REQUEST = "IS_AIA_REQUEST";
Expand Down
4 changes: 2 additions & 2 deletions server-spi/src/main/java/org/keycloak/models/UserModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ interface UserRemovedEvent extends ProviderEvent {
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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
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}.
*/
Set<String> getRequiredActions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.keycloak.authentication.InitiatedActionSupport;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
Expand Down Expand Up @@ -90,7 +91,7 @@ public void requiredActionChallenge(RequiredActionContext context) {

EventBuilder event = context.getEvent().clone();
event.event(EventType.VERIFY_PROFILE);
event.detail("fields_to_update", collectFields(errors));
event.detail(Details.FIELDS_TO_UPDATE, collectFields(errors));
event.success();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,8 @@ protected void createCommonAttributes(Theme theme, Locale locale, Properties mes
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,16 @@
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -1032,45 +1035,40 @@ public static Response finishedRequiredActions(KeycloakSession session, Authenti
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,
final HttpRequest request, final EventBuilder event) {
final RealmModel realm = authSession.getRealm();
final UserModel user = authSession.getAuthenticatedUser();
final ClientModel client = authSession.getClient();
final HttpRequest request, final EventBuilder event) {
final var realm = authSession.getRealm();
final var user = authSession.getAuthenticatedUser();

evaluateRequiredActionTriggers(session, authSession, request, event, realm, user);

Optional<String> reqAction = user.getRequiredActionsStream().findFirst();
if (reqAction.isPresent()) {
return reqAction.get();
}
if (!authSession.getRequiredActions().isEmpty()) {
return authSession.getRequiredActions().iterator().next();
}

String kcAction = authSession.getClientNote(Constants.KC_ACTION);
if (kcAction != null) {
return kcAction;
final var kcAction = authSession.getClientNote(Constants.KC_ACTION);
final var nextApplicableAction =
getFirstApplicableRequiredAction(realm, authSession, user, kcAction);
if (nextApplicableAction != null) {
return nextApplicableAction.getAlias();
}

final var client = authSession.getClient();
if (client.isConsentRequired() || isOAuth2DeviceVerificationFlow(authSession)) {

UserConsentModel grantedConsent = getEffectiveGrantedConsent(session, authSession);

// 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()) {
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);
} else {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED);
}
return null;

}


Expand All @@ -1097,24 +1095,21 @@ private static UserConsentModel getEffectiveGrantedConsent(KeycloakSession sessi


public static Response actionRequired(final KeycloakSession session, final AuthenticationSessionModel authSession,
final HttpRequest request, final EventBuilder event) {
final RealmModel realm = authSession.getRealm();
final UserModel user = authSession.getAuthenticatedUser();
final ClientModel client = authSession.getClient();
final HttpRequest request, final EventBuilder event) {
final var realm = authSession.getRealm();
final var user = authSession.getAuthenticatedUser();

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());

Stream<String> requiredActions = user.getRequiredActionsStream();
Response action = executionActions(session, authSession, request, event, realm, user, requiredActions);
if (action != null) return action;
final var actionResponse = executionActions(session, authSession, request, event, realm, user);
if (actionResponse != null) {
return actionResponse;
}

// executionActions() method should remove any duplicate actions that might be in the clientSession
action = executionActions(session, authSession, request, event, realm, user, authSession.getRequiredActions().stream());
if (action != null) return action;
final var client = authSession.getClient();
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());

// 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",
Expand All @@ -1123,13 +1118,15 @@ public static Response actionRequired(final KeycloakSession session, final Authe

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
if (clientScopesToApprove.size() > 0) {
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());
authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution);

Expand All @@ -1140,7 +1137,8 @@ public static Response actionRequired(final KeycloakSession session, final Authe
.setAccessRequest(clientScopesToApprove)
.createOAuthGrant();
} 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);
}
} else {
Expand Down Expand Up @@ -1220,26 +1218,14 @@ public static RequiredActionProvider createRequiredAction(RequiredActionContextR


protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
Stream<String> requiredActions) {

Optional<Response> response = sortRequiredActionsByPriority(realm, requiredActions)
.map(model -> executeAction(session, authSession, model, request, event, realm, user, false))
.filter(Objects::nonNull).findFirst();
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);
}
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user) {
final var kcAction = authSession.getClientNote(Constants.KC_ACTION);
final var firstApplicableRequiredAction =
getFirstApplicableRequiredAction(realm, authSession, user, kcAction);

logger.debugv("Requested action {0} not configured for realm", kcAction);
setKcActionStatus(kcAction, RequiredActionContext.KcActionStatus.ERROR, authSession);
if (firstApplicableRequiredAction != null) {
return executeAction(session, authSession, firstApplicableRequiredAction, request, event, realm, user,
kcAction != null);
}

return null;
Expand Down Expand Up @@ -1304,17 +1290,81 @@ else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
return null;
}

private static Stream<RequiredActionProviderModel> sortRequiredActionsByPriority(RealmModel realm, Stream<String> requiredActions) {
return requiredActions.map(action -> {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
if (model == null) {
logger.warnv("Could not find configuration for Required Action {0}, did you forget to register it?", action);
}
return model;
})
private static RequiredActionProviderModel getFirstApplicableRequiredAction(final RealmModel realm,
final AuthenticationSessionModel authSession, final UserModel user, final String kcAction) {
final var applicableRequiredActionsSorted =
getApplicableRequiredActionsSorted(realm, authSession, user, kcAction);

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 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(RequiredActionProviderModel::isEnabled)
.sorted(RequiredActionProviderModel.RequiredActionComparator.SINGLETON);
.collect(Collectors.toMap(RequiredActionProviderModel::getAlias, Function.identity()));

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,
Expand Down Expand Up @@ -1524,6 +1574,12 @@ public static void setKcActionStatus(String executedProviderId, RequiredActionCo
}
}

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) {
RealmModel realm = session.getContext().getRealm();
if (realm.isBruteForceProtected()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,8 @@ private Response interruptionResponse(RequiredActionContextResult context, Authe
}

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();
boolean userRequestedCancelAIA = formData.getFirst(CANCEL_AIA) != null;
return userRequestedCancelAIA;
Expand Down
Loading
0