From 88a5c96fff05f640145c566538511966b9b57ca5 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 3 Sep 2024 16:26:23 +0200 Subject: [PATCH] Add `kc_action` to redirect URI after a required action is cancelled (#31925) Closes #31894 Signed-off-by: Thomas Darimont --- .../server_admin/topics/users/con-aia.adoc | 5 ++++- .../topics/changes/changes-26_0_0.adoc | 7 +++++++ js/libs/keycloak-js/dist/keycloak.d.ts | 4 +++- js/libs/keycloak-js/src/keycloak.js | 10 +++++----- .../protocol/oidc/OIDCLoginProtocol.java | 5 +++++ .../AbstractAppInitiatedActionTest.java | 18 +++++++++++++----- ...tractAppInitiatedActionUpdateEmailTest.java | 1 + 7 files changed, 38 insertions(+), 12 deletions(-) diff --git a/docs/documentation/server_admin/topics/users/con-aia.adoc b/docs/documentation/server_admin/topics/users/con-aia.adoc index e2b6fa8f4a..63c60203f0 100644 --- a/docs/documentation/server_admin/topics/users/con-aia.adoc +++ b/docs/documentation/server_admin/topics/users/con-aia.adoc @@ -12,7 +12,10 @@ and then is immediately redirected back to the application. However, AIA allows done even if the user is already authenticated on the client and has an active SSO session. It is triggered by adding the `kc_action` parameter to the OIDC login URL with the value containing the requested action. For instance `kc_action=UPDATE_PASSWORD` parameter. -NOTE: The `kc_action` parameter is a {project_name} proprietary mechanism unsupported by the OIDC specification. +A user may cancel an application initiated action. In this case the user is redirected back to the client application. +The redirect URI will contain the query parameters `kc_action_status=cancelled` and `kc_action` with the name of the cancelled action. + +NOTE: The `kc_action` and `kc_action_status` parameters are a {project_name} proprietary mechanism unsupported by the OIDC specification. NOTE: Application initiated actions are supported only for OIDC clients. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc index 8048a9589d..c5e5e86e6d 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc @@ -125,6 +125,13 @@ It used to be difficult to regain access to a {project_name} instance when all a Consequently, the environment variables `KEYCLOAK_ADMIN` and `KEYCLOAK_ADMIN_PASSWORD` have been deprecated. You should use `KC_BOOTSTRAP_ADMIN_USERNAME` and `KC_BOOTSTRAP_ADMIN_PASSWORD` instead. These are also general options, so they may be specified via the cli or other config sources, for example `--bootstrap-admin-username=admin`. For more information, see the new https://www.keycloak.org/server/bootstrap-admin-recovery[Bootstrap admin and recovery] guide. += Application Initiated Required Action redirect now contains kc_action Parameter + +The required action provider name is now returned via the `kc_action` parameter when redirecting back from an application initiated required action execution. +This eases the detection of which required action was executed for a client. The outcome of the execution can be determined via the `kc_action_status` parameter. + +Note: This feature required changes to the Keycloak JS adapter, therefore it is recommended to upgrade to the latest version of the adapter if you want to make use of this feature. + = Deprecations in `keycloak-services` module The class `UserSessionCrossDCManager` is deprecated and planned to be removed in a future version of {project_name}. diff --git a/js/libs/keycloak-js/dist/keycloak.d.ts b/js/libs/keycloak-js/dist/keycloak.d.ts index bc0947c2fd..652effe5b4 100644 --- a/js/libs/keycloak-js/dist/keycloak.d.ts +++ b/js/libs/keycloak-js/dist/keycloak.d.ts @@ -536,8 +536,10 @@ declare class Keycloak { /** * Called when a AIA has been requested by the application. + * @param status the outcome of the required action + * @param action the alias name of the required action, e.g. UPDATE_PASSWORD, CONFIGURE_TOTP etc. */ - onActionUpdate?(status: 'success'|'cancelled'|'error'): void; + onActionUpdate?(status: 'success'|'cancelled'|'error', action?: string): void; /** * Called to initialize the adapter. diff --git a/js/libs/keycloak-js/src/keycloak.js b/js/libs/keycloak-js/src/keycloak.js index 720b4c0deb..af26da0459 100755 --- a/js/libs/keycloak-js/src/keycloak.js +++ b/js/libs/keycloak-js/src/keycloak.js @@ -751,7 +751,7 @@ function Keycloak (config) { var timeLocal = new Date().getTime(); if (oauth['kc_action_status']) { - kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status']); + kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status'], oauth['kc_action']); } if (error) { @@ -1080,13 +1080,13 @@ function Keycloak (config) { var supportedParams; switch (kc.flow) { case 'standard': - supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'iss']; + supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'kc_action', 'iss']; break; case 'implicit': - supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'iss']; + supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss']; break; case 'hybrid': - supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'iss']; + supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss']; break; } @@ -1441,7 +1441,7 @@ function Keycloak (config) { var getCordovaRedirectUri = function() { return kc.redirectUri || 'http://localhost'; } - + return { login: function(options) { var promise = createPromise(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index e9dedfa2d4..37e2407c08 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -24,6 +24,7 @@ import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.TokenIdGenerator; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.Time; import org.keycloak.connections.httpclient.HttpClientProvider; @@ -229,6 +230,10 @@ public class OIDCLoginProtocol implements LoginProtocol { String kcActionStatus = authSession.getClientNote(Constants.KC_ACTION_STATUS); if (kcActionStatus != null) { + String requiredActionAlias = authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION); + if (requiredActionAlias != null) { + redirectUri.addParam(Constants.KC_ACTION, requiredActionAlias); + } redirectUri.addParam(Constants.KC_ACTION_STATUS, kcActionStatus); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionTest.java index b79a0699d1..a31ff5d60f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionTest.java @@ -79,7 +79,17 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe protected void assertKcActionStatus(String expectedStatus) { assertThat(appPage.getRequestType(),is(RequestType.AUTH_RESPONSE)); + String kcActionStatus = getCurrentUrlParam(KC_ACTION_STATUS); + assertThat(kcActionStatus, is(expectedStatus)); + } + protected void assertKcAction(String expectedKcAction) { + assertThat(appPage.getRequestType(),is(RequestType.AUTH_RESPONSE)); + String kcAction = getCurrentUrlParam(KC_ACTION); + assertThat(kcAction, is(expectedKcAction)); + } + + protected String getCurrentUrlParam(String paramName) { final URI url; try { url = new URI(this.driver.getCurrentUrl()); @@ -88,14 +98,12 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe } List pairs = URLEncodedUtils.parse(url, StandardCharsets.UTF_8); - String kcActionStatus = null; for (NameValuePair p : pairs) { - if (p.getName().equals(KC_ACTION_STATUS)) { - kcActionStatus = p.getValue(); - break; + if (p.getName().equals(paramName)) { + return p.getValue(); } } - assertThat(expectedStatus, is(kcActionStatus)); + return null; } protected void assertSilentCancelMessage() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java index 4dd3e6149f..49e3cb8fe7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AbstractAppInitiatedActionUpdateEmailTest.java @@ -79,6 +79,7 @@ public abstract class AbstractAppInitiatedActionUpdateEmailTest extends Abstract emailUpdatePage.assertCurrent(); emailUpdatePage.cancel(); + assertKcAction(UserModel.RequiredAction.UPDATE_EMAIL.name()); assertKcActionStatus("cancelled"); // assert nothing was updated in persistent store