Add kc_action to redirect URI after a required action is cancelled (#31925)

Closes #31894

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
This commit is contained in:
Thomas Darimont 2024-09-03 16:26:23 +02:00 committed by GitHub
parent dad4477995
commit 88a5c96fff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 38 additions and 12 deletions

View file

@ -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. 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. 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. NOTE: Application initiated actions are supported only for OIDC clients.

View file

@ -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. 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 = Deprecations in `keycloak-services` module
The class `UserSessionCrossDCManager` is deprecated and planned to be removed in a future version of {project_name}. The class `UserSessionCrossDCManager` is deprecated and planned to be removed in a future version of {project_name}.

View file

@ -536,8 +536,10 @@ declare class Keycloak {
/** /**
* Called when a AIA has been requested by the application. * 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. * Called to initialize the adapter.

View file

@ -751,7 +751,7 @@ function Keycloak (config) {
var timeLocal = new Date().getTime(); var timeLocal = new Date().getTime();
if (oauth['kc_action_status']) { 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) { if (error) {
@ -1080,13 +1080,13 @@ function Keycloak (config) {
var supportedParams; var supportedParams;
switch (kc.flow) { switch (kc.flow) {
case 'standard': case 'standard':
supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'iss']; supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'kc_action', 'iss'];
break; break;
case 'implicit': 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; break;
case 'hybrid': 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; break;
} }
@ -1441,7 +1441,7 @@ function Keycloak (config) {
var getCordovaRedirectUri = function() { var getCordovaRedirectUri = function() {
return kc.redirectUri || 'http://localhost'; return kc.redirectUri || 'http://localhost';
} }
return { return {
login: function(options) { login: function(options) {
var promise = createPromise(); var promise = createPromise();

View file

@ -24,6 +24,7 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.TokenIdGenerator; import org.keycloak.TokenIdGenerator;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.connections.httpclient.HttpClientProvider;
@ -229,6 +230,10 @@ public class OIDCLoginProtocol implements LoginProtocol {
String kcActionStatus = authSession.getClientNote(Constants.KC_ACTION_STATUS); String kcActionStatus = authSession.getClientNote(Constants.KC_ACTION_STATUS);
if (kcActionStatus != null) { 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); redirectUri.addParam(Constants.KC_ACTION_STATUS, kcActionStatus);
} }

View file

@ -79,7 +79,17 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
protected void assertKcActionStatus(String expectedStatus) { protected void assertKcActionStatus(String expectedStatus) {
assertThat(appPage.getRequestType(),is(RequestType.AUTH_RESPONSE)); 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; final URI url;
try { try {
url = new URI(this.driver.getCurrentUrl()); url = new URI(this.driver.getCurrentUrl());
@ -88,14 +98,12 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
} }
List<NameValuePair> pairs = URLEncodedUtils.parse(url, StandardCharsets.UTF_8); List<NameValuePair> pairs = URLEncodedUtils.parse(url, StandardCharsets.UTF_8);
String kcActionStatus = null;
for (NameValuePair p : pairs) { for (NameValuePair p : pairs) {
if (p.getName().equals(KC_ACTION_STATUS)) { if (p.getName().equals(paramName)) {
kcActionStatus = p.getValue(); return p.getValue();
break;
} }
} }
assertThat(expectedStatus, is(kcActionStatus)); return null;
} }
protected void assertSilentCancelMessage() { protected void assertSilentCancelMessage() {

View file

@ -79,6 +79,7 @@ public abstract class AbstractAppInitiatedActionUpdateEmailTest extends Abstract
emailUpdatePage.assertCurrent(); emailUpdatePage.assertCurrent();
emailUpdatePage.cancel(); emailUpdatePage.cancel();
assertKcAction(UserModel.RequiredAction.UPDATE_EMAIL.name());
assertKcActionStatus("cancelled"); assertKcActionStatus("cancelled");
// assert nothing was updated in persistent store // assert nothing was updated in persistent store