From 74a02dd82fcd0a5b4e4b7596a1476272a2d6f142 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Mon, 7 Oct 2024 15:25:30 +0200 Subject: [PATCH] Return next action if the current action is not supported in AIA Closes #33513 Signed-off-by: rmartinc --- .../managers/AuthenticationManager.java | 17 +++++---- .../actions/AppInitiatedActionTest.java | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index e41a364013b2..a832b93a10ec 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -1052,7 +1052,7 @@ public static String nextRequiredAction(final KeycloakSession session, final Aut final var kcAction = authSession.getClientNote(Constants.KC_ACTION); final var nextApplicableAction = - getFirstApplicableRequiredAction(realm, authSession, user, kcAction); + getFirstApplicableRequiredAction(realm, authSession, user, kcAction, new HashSet<>()); if (nextApplicableAction != null) { return nextApplicableAction.getAlias(); } @@ -1232,7 +1232,7 @@ protected static Response executionActions(KeycloakSession session, Authenticati HttpRequest request, EventBuilder event, RealmModel realm, UserModel user, Set ignoredActions) { final var kcAction = authSession.getClientNote(Constants.KC_ACTION); final var firstApplicableRequiredAction = - getFirstApplicableRequiredAction(realm, authSession, user, kcAction); + getFirstApplicableRequiredAction(realm, authSession, user, kcAction, ignoredActions); if (firstApplicableRequiredAction != null) { return executeAction(session, authSession, firstApplicableRequiredAction, request, event, realm, user, @@ -1265,11 +1265,13 @@ private static Response executeAction(KeycloakSession session, AuthenticationSes if (actionProvider.initiatedActionSupport() == InitiatedActionSupport.NOT_SUPPORTED) { logger.debugv("Requested action {0} does not support being invoked with kc_action", factory.getId()); setKcActionStatus(factory.getId(), RequiredActionContext.KcActionStatus.ERROR, authSession); - return null; + ignoredActions.add(factory.getId()); + return nextActionAfterAuthentication(session, authSession, session.getContext().getConnection(), request, session.getContext().getUri(), event, ignoredActions); } else if (!model.isEnabled()) { logger.debugv("Requested action {0} is disabled and can't be invoked with kc_action", factory.getId()); setKcActionStatus(factory.getId(), RequiredActionContext.KcActionStatus.ERROR, authSession); - return null; + ignoredActions.add(factory.getId()); + return nextActionAfterAuthentication(session, authSession, session.getContext().getConnection(), request, session.getContext().getUri(), event, ignoredActions); } else { authSession.setClientNote(Constants.KC_ACTION_EXECUTING, factory.getId()); } @@ -1311,9 +1313,9 @@ else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) { } private static RequiredActionProviderModel getFirstApplicableRequiredAction(final RealmModel realm, - final AuthenticationSessionModel authSession, final UserModel user, final String kcAction) { + final AuthenticationSessionModel authSession, final UserModel user, final String kcAction, final Set ignoredActions) { final var applicableRequiredActionsSorted = - getApplicableRequiredActionsSorted(realm, authSession, user, kcAction); + getApplicableRequiredActionsSorted(realm, authSession, user, kcAction, ignoredActions); final RequiredActionProviderModel firstApplicableRequiredAction; if (applicableRequiredActionsSorted.isEmpty()) { @@ -1328,7 +1330,7 @@ private static RequiredActionProviderModel getFirstApplicableRequiredAction(fina } private static List getApplicableRequiredActionsSorted(final RealmModel realm, - final AuthenticationSessionModel authSession, final UserModel user, final String kcActionAlias) { + final AuthenticationSessionModel authSession, final UserModel user, final String kcActionAlias, final Set ignoredActions) { final Set nonInitiatedActionAliases = new HashSet<>(); nonInitiatedActionAliases.addAll(user.getRequiredActionsStream().toList()); nonInitiatedActionAliases.addAll(authSession.getRequiredActions()); @@ -1336,6 +1338,7 @@ private static List getApplicableRequiredActionsSor final var applicableNonInitiatedActions = nonInitiatedActionAliases.stream() .map(alias -> getApplicableRequiredAction(realm, alias)) .filter(Objects::nonNull) + .filter(model -> !ignoredActions.contains(model.getProviderId())) .collect(Collectors.toMap(RequiredActionProviderModel::getAlias, Function.identity())); RequiredActionProviderModel kcAction = null; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTest.java index 2b46e00fbabb..4f146b0af78b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionTest.java @@ -16,9 +16,11 @@ */ package org.keycloak.testsuite.actions; +import java.io.IOException; import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; @@ -27,6 +29,8 @@ import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.VerifyEmailPage; +import org.keycloak.testsuite.updaters.UserAttributeUpdater; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -49,6 +53,9 @@ public void configureTestRealm(RealmRepresentation testRealm) { @Page protected LoginPage loginPage; + @Page + protected VerifyEmailPage verifyEmailPage; + @Test public void executeUnknownAction() { oauth.kcAction("nosuch").openLoginForm(); @@ -93,4 +100,32 @@ public void executeDisabledAction() { testRealm().flows().updateRequiredAction("CONFIGURE_TOTP", configureTotp); } } + + @Test + public void executeActionWithVerifyEmailUnsupportedAIA() throws IOException { + RealmResource realm = testRealm(); + RequiredActionProviderRepresentation model = realm.flows().getRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name()); + int prevPriority = model.getPriority(); + + try (UserAttributeUpdater userUpdater = UserAttributeUpdater + .forUserByUsername(realm, "test-user@localhost") + .setRequiredActions(UserModel.RequiredAction.VERIFY_EMAIL).update()) { + // Set max priority for verify email (AIA not supported) to be executed before update password + model.setPriority(1); + realm.flows().updateRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name(), model); + + oauth.kcAction(UserModel.RequiredAction.UPDATE_PASSWORD.name()).openLoginForm(); + loginPage.login("test-user@localhost", "password"); + + // the update password should be displayed + passwordUpdatePage.assertCurrent(); + passwordUpdatePage.changePassword("password", "password"); + + // once the AIA password is executed the verify profile should be displayed for the login + verifyEmailPage.assertCurrent(); + } finally { + model.setPriority(prevPriority); + realm.flows().updateRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL.name(), model); + } + } }