KEYCLOAK-10854: App-initiated actions Phase I
This commit is contained in:
parent
6c79bdee41
commit
bc818367a1
25 changed files with 142 additions and 101 deletions
|
@ -21,5 +21,5 @@ package org.keycloak.authentication;
|
||||||
* @author Stan Silvert
|
* @author Stan Silvert
|
||||||
*/
|
*/
|
||||||
public enum InitiatedActionSupport {
|
public enum InitiatedActionSupport {
|
||||||
SUPPORTED, NOT_SUPPORTED, CONSENT_REQUIRED
|
SUPPORTED, NOT_SUPPORTED
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,8 @@ public interface RequiredActionContext {
|
||||||
CHALLENGE,
|
CHALLENGE,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
IGNORE,
|
IGNORE,
|
||||||
FAILURE
|
FAILURE,
|
||||||
|
CANCELED_AIA
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,4 +140,10 @@ public interface RequiredActionContext {
|
||||||
*/
|
*/
|
||||||
void ignore();
|
void ignore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark application-initiated action as canceled by the user.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void cancelAIA();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
|
|
||||||
package org.keycloak.authentication;
|
package org.keycloak.authentication;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RequiredAction provider. Required actions are one-time actions that a user must perform before they are logged in.
|
* RequiredAction provider. Required actions are one-time actions that a user must perform before they are logged in.
|
||||||
|
@ -36,6 +38,18 @@ public interface RequiredActionProvider extends Provider {
|
||||||
return InitiatedActionSupport.NOT_SUPPORTED;
|
return InitiatedActionSupport.NOT_SUPPORTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to let the action know that an application-initiated action
|
||||||
|
* was canceled.
|
||||||
|
*
|
||||||
|
* @param session The Keycloak session.
|
||||||
|
* @param authSession The authentication session.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
default void initiatedActionCanceled(KeycloakSession session, AuthenticationSessionModel authSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every time a user authenticates. This checks to see if this required action should be triggered.
|
* Called every time a user authenticates. This checks to see if this required action should be triggered.
|
||||||
* The implementation of this method is responsible for setting the required action on the UserModel.
|
* The implementation of this method is responsible for setting the required action on the UserModel.
|
||||||
|
|
|
@ -45,7 +45,6 @@ import org.keycloak.migration.migrators.MigrateTo4_0_0;
|
||||||
import org.keycloak.migration.migrators.MigrateTo4_2_0;
|
import org.keycloak.migration.migrators.MigrateTo4_2_0;
|
||||||
import org.keycloak.migration.migrators.MigrateTo4_6_0;
|
import org.keycloak.migration.migrators.MigrateTo4_6_0;
|
||||||
import org.keycloak.migration.migrators.MigrateTo6_0_0;
|
import org.keycloak.migration.migrators.MigrateTo6_0_0;
|
||||||
import org.keycloak.migration.migrators.MigrateTo7_0_0;
|
|
||||||
import org.keycloak.migration.migrators.Migration;
|
import org.keycloak.migration.migrators.Migration;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -82,8 +81,7 @@ public class MigrationModelManager {
|
||||||
new MigrateTo4_0_0(),
|
new MigrateTo4_0_0(),
|
||||||
new MigrateTo4_2_0(),
|
new MigrateTo4_2_0(),
|
||||||
new MigrateTo4_6_0(),
|
new MigrateTo4_6_0(),
|
||||||
new MigrateTo6_0_0(),
|
new MigrateTo6_0_0()
|
||||||
new MigrateTo7_0_0()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static void migrate(KeycloakSession session) {
|
public static void migrate(KeycloakSession session) {
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.keycloak.migration.migrators;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.migration.ModelVersion;
|
|
||||||
import org.keycloak.models.AccountRoles;
|
|
||||||
import org.keycloak.models.ClientModel;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.RoleModel;
|
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the migration necessary for version 6.0.0.
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
|
|
||||||
*/
|
|
||||||
public class MigrateTo7_0_0 implements Migration {
|
|
||||||
|
|
||||||
public static final ModelVersion VERSION = new ModelVersion("7.0.0");
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(MigrateTo7_0_0.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ModelVersion getVersion() {
|
|
||||||
return VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void migrate(KeycloakSession session) {
|
|
||||||
session.realms().getRealms().stream().forEach(r -> {
|
|
||||||
migrateRealm(session, r, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
|
|
||||||
migrateRealm(session, realm, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void migrateRealm(KeycloakSession session, RealmModel realm, boolean jsn) {
|
|
||||||
ClientModel account = realm.getClientByClientId("account");
|
|
||||||
if (account != null) {
|
|
||||||
RoleModel role = account.addRole(AccountRoles.INITIATE_ACTION);
|
|
||||||
role.setDescription("${role_" + AccountRoles.INITIATE_ACTION + "}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,6 +27,6 @@ public interface AccountRoles {
|
||||||
String INITIATE_ACTION = "initiate-action";
|
String INITIATE_ACTION = "initiate-action";
|
||||||
String MANAGE_ACCOUNT_LINKS = "manage-account-links";
|
String MANAGE_ACCOUNT_LINKS = "manage-account-links";
|
||||||
|
|
||||||
String[] ALL = {VIEW_PROFILE, MANAGE_ACCOUNT, INITIATE_ACTION};
|
String[] ALL = {VIEW_PROFILE, MANAGE_ACCOUNT};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ public final class Constants {
|
||||||
public static final String KEY = "key";
|
public static final String KEY = "key";
|
||||||
public static final String KC_ACTION = "kc_action";
|
public static final String KC_ACTION = "kc_action";
|
||||||
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";
|
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";
|
||||||
|
public static final String AIA_SILENT_CANCEL = "silent_cancel";
|
||||||
|
|
||||||
public static final String SKIP_LINK = "skipLink";
|
public static final String SKIP_LINK = "skipLink";
|
||||||
public static final String TEMPLATE_ATTR_ACTION_URI = "actionUri";
|
public static final String TEMPLATE_ATTR_ACTION_URI = "actionUri";
|
||||||
|
|
|
@ -43,6 +43,14 @@ public interface LoginProtocol extends Provider {
|
||||||
* Login cancelled by the user
|
* Login cancelled by the user
|
||||||
*/
|
*/
|
||||||
CANCELLED_BY_USER,
|
CANCELLED_BY_USER,
|
||||||
|
/**
|
||||||
|
* Applications-initiated action was canceled by the user
|
||||||
|
*/
|
||||||
|
CANCELLED_AIA,
|
||||||
|
/**
|
||||||
|
* Applications-initiated action was canceled by the user. Do not send error.
|
||||||
|
*/
|
||||||
|
CANCELLED_AIA_SILENT,
|
||||||
/**
|
/**
|
||||||
* Consent denied by the user
|
* Consent denied by the user
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -137,6 +137,11 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
||||||
status = Status.IGNORE;
|
status = Status.IGNORE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelAIA() {
|
||||||
|
status = Status.CANCELED_AIA;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getActionUrl(String code) {
|
public URI getActionUrl(String code) {
|
||||||
ClientModel client = authenticationSession.getClient();
|
ClientModel client = authenticationSession.getClient();
|
||||||
|
|
|
@ -269,9 +269,18 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
|
|
||||||
String redirect = authSession.getRedirectUri();
|
String redirect = authSession.getRedirectUri();
|
||||||
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
|
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
|
||||||
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode).addParam(OAuth2Constants.ERROR, translateError(error));
|
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode);
|
||||||
if (state != null)
|
|
||||||
|
if (error != Error.CANCELLED_AIA_SILENT) {
|
||||||
|
redirectUri.addParam(OAuth2Constants.ERROR, translateError(error));
|
||||||
|
}
|
||||||
|
if (error == Error.CANCELLED_AIA) {
|
||||||
|
redirectUri.addParam(OAuth2Constants.ERROR_DESCRIPTION, "User cancelled aplication-initiated action.");
|
||||||
|
}
|
||||||
|
if (state != null) {
|
||||||
redirectUri.addParam(OAuth2Constants.STATE, state);
|
redirectUri.addParam(OAuth2Constants.STATE, state);
|
||||||
|
}
|
||||||
|
|
||||||
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
|
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
|
||||||
return redirectUri.build();
|
return redirectUri.build();
|
||||||
}
|
}
|
||||||
|
@ -279,6 +288,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
private String translateError(Error error) {
|
private String translateError(Error error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case CANCELLED_BY_USER:
|
case CANCELLED_BY_USER:
|
||||||
|
case CANCELLED_AIA:
|
||||||
|
return OAuthErrorException.INTERACTION_REQUIRED;
|
||||||
case CONSENT_DENIED:
|
case CONSENT_DENIED:
|
||||||
return OAuthErrorException.ACCESS_DENIED;
|
return OAuthErrorException.ACCESS_DENIED;
|
||||||
case PASSIVE_INTERACTION_REQUIRED:
|
case PASSIVE_INTERACTION_REQUIRED:
|
||||||
|
|
|
@ -225,6 +225,7 @@ public class SamlProtocol implements LoginProtocol {
|
||||||
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
|
private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case CANCELLED_BY_USER:
|
case CANCELLED_BY_USER:
|
||||||
|
case CANCELLED_AIA:
|
||||||
case CONSENT_DENIED:
|
case CONSENT_DENIED:
|
||||||
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
|
return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
|
||||||
case PASSIVE_INTERACTION_REQUIRED:
|
case PASSIVE_INTERACTION_REQUIRED:
|
||||||
|
|
|
@ -45,7 +45,6 @@ import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AccountRoles;
|
|
||||||
import org.keycloak.models.ActionTokenKeyModel;
|
import org.keycloak.models.ActionTokenKeyModel;
|
||||||
import org.keycloak.models.ActionTokenStoreProvider;
|
import org.keycloak.models.ActionTokenStoreProvider;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
|
@ -66,6 +65,7 @@ import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.LoginProtocol.Error;
|
import org.keycloak.protocol.LoginProtocol.Error;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.services.ForbiddenException;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
@ -96,6 +96,7 @@ import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import org.keycloak.models.AccountRoles;
|
||||||
|
|
||||||
import static org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX;
|
import static org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX;
|
||||||
|
|
||||||
|
@ -132,6 +133,7 @@ public class AuthenticationManager {
|
||||||
private static final TokenTypeCheck VALIDATE_IDENTITY_COOKIE = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_KEYCLOAK_ID);
|
private static final TokenTypeCheck VALIDATE_IDENTITY_COOKIE = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_KEYCLOAK_ID);
|
||||||
private static final String AIA_REQUEST = LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + Constants.KC_ACTION;
|
private static final String AIA_REQUEST = LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + Constants.KC_ACTION;
|
||||||
public static final String IS_AIA_REQUEST = LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + Constants.IS_AIA_REQUEST;
|
public static final String IS_AIA_REQUEST = LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + Constants.IS_AIA_REQUEST;
|
||||||
|
public static final String IS_SILENT_CANCEL = LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + Constants.AIA_SILENT_CANCEL;
|
||||||
|
|
||||||
public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) {
|
public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) {
|
||||||
if (userSession == null) {
|
if (userSession == null) {
|
||||||
|
@ -1144,6 +1146,11 @@ public class AuthenticationManager {
|
||||||
public void ignore() {
|
public void ignore() {
|
||||||
throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
|
throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelAIA() {
|
||||||
|
throw new RuntimeException("Not allowed to call cancelAIA() within evaluateTriggers()");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
evaluateApplicationInitiatedActionTrigger(session, provider, model, authSession);
|
evaluateApplicationInitiatedActionTrigger(session, provider, model, authSession);
|
||||||
|
@ -1166,9 +1173,8 @@ public class AuthenticationManager {
|
||||||
// make sure you are evaluating the action that was requested
|
// make sure you are evaluating the action that was requested
|
||||||
if (!aia.equalsIgnoreCase(model.getProviderId())) return;
|
if (!aia.equalsIgnoreCase(model.getProviderId())) return;
|
||||||
|
|
||||||
if (session.getContext().getClient().getRole(AccountRoles.INITIATE_ACTION) == null) {
|
if (session.getContext().getClient().getRole(AccountRoles.MANAGE_ACCOUNT) == null) {
|
||||||
logger.error("Client must have initiate-action role to perform application-initiated action.");
|
throw new ForbiddenException("Client must have manage-account role to perform application-initiated actions.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authSession.addRequiredAction(model.getProviderId());
|
authSession.addRequiredAction(model.getProviderId());
|
||||||
|
|
|
@ -102,6 +102,7 @@ import java.util.Map;
|
||||||
|
|
||||||
import static org.keycloak.authentication.actiontoken.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS;
|
import static org.keycloak.authentication.actiontoken.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS;
|
||||||
import static org.keycloak.services.managers.AuthenticationManager.IS_AIA_REQUEST;
|
import static org.keycloak.services.managers.AuthenticationManager.IS_AIA_REQUEST;
|
||||||
|
import static org.keycloak.services.managers.AuthenticationManager.IS_SILENT_CANCEL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -993,7 +994,8 @@ public class LoginActionsService {
|
||||||
Response response;
|
Response response;
|
||||||
|
|
||||||
if (isCancelAppInitiatedAction(authSession, context)) {
|
if (isCancelAppInitiatedAction(authSession, context)) {
|
||||||
context.failure();
|
provider.initiatedActionCanceled(session, authSession);
|
||||||
|
context.cancelAIA();
|
||||||
} else {
|
} else {
|
||||||
provider.processAction(context);
|
provider.processAction(context);
|
||||||
}
|
}
|
||||||
|
@ -1014,15 +1016,11 @@ public class LoginActionsService {
|
||||||
} else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
|
} else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
|
||||||
response = context.getChallenge();
|
response = context.getChallenge();
|
||||||
} else if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
|
} else if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
|
||||||
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol());
|
response = interruptionResponse(context, authSession, action, Error.CONSENT_DENIED);
|
||||||
protocol.setRealm(context.getRealm())
|
} else if (isSilentAIACancel(authSession, context)) {
|
||||||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
response = interruptionResponse(context, authSession, action, Error.CANCELLED_AIA_SILENT);
|
||||||
.setUriInfo(context.getUriInfo())
|
} else if (context.getStatus() == RequiredActionContext.Status.CANCELED_AIA) {
|
||||||
.setEventBuilder(event);
|
response = interruptionResponse(context, authSession, action, Error.CANCELLED_AIA);
|
||||||
|
|
||||||
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
|
|
||||||
response = protocol.sendError(authSession, Error.CONSENT_DENIED);
|
|
||||||
event.error(Errors.REJECTED_BY_USER);
|
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("Unreachable");
|
throw new RuntimeException("Unreachable");
|
||||||
}
|
}
|
||||||
|
@ -1030,6 +1028,19 @@ public class LoginActionsService {
|
||||||
return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true, request);
|
return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Response interruptionResponse(RequiredActionContextResult context, AuthenticationSessionModel authSession, String action, Error error) {
|
||||||
|
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol());
|
||||||
|
protocol.setRealm(context.getRealm())
|
||||||
|
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
||||||
|
.setUriInfo(context.getUriInfo())
|
||||||
|
.setEventBuilder(event);
|
||||||
|
|
||||||
|
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
|
||||||
|
|
||||||
|
event.error(Errors.REJECTED_BY_USER);
|
||||||
|
return protocol.sendError(authSession, error);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isCancelAppInitiatedAction(AuthenticationSessionModel authSession, RequiredActionContextResult context) {
|
private boolean isCancelAppInitiatedAction(AuthenticationSessionModel authSession, RequiredActionContextResult context) {
|
||||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
|
||||||
|
@ -1039,4 +1050,12 @@ public class LoginActionsService {
|
||||||
return isAIARequest && userRequestedCancelAIA;
|
return isAIARequest && userRequestedCancelAIA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSilentAIACancel(AuthenticationSessionModel authSession, RequiredActionContextResult context) {
|
||||||
|
String silentCancel = authSession.getClientNote(IS_SILENT_CANCEL);
|
||||||
|
boolean isSilentCancel = "true".equalsIgnoreCase(silentCancel);
|
||||||
|
boolean isAIACancel = isCancelAppInitiatedAction(authSession, context);
|
||||||
|
|
||||||
|
return isSilentCancel && isAIACancel;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,8 +50,13 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void doAIA() {
|
protected void doAIA() {
|
||||||
|
doAIA(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void doAIA(boolean silentCancel) {
|
||||||
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
|
UriBuilder builder = OIDCLoginProtocolService.authUrl(authServerPage.createUriBuilder());
|
||||||
String uri = builder.queryParam("kc_action", this.aiaAction)
|
String uri = builder.queryParam("kc_action", this.aiaAction)
|
||||||
|
.queryParam("silent_cancel", Boolean.toString(silentCancel))
|
||||||
.queryParam("response_type", "code")
|
.queryParam("response_type", "code")
|
||||||
.queryParam("client_id", "test-app")
|
.queryParam("client_id", "test-app")
|
||||||
.queryParam("scope", "openid")
|
.queryParam("scope", "openid")
|
||||||
|
@ -64,4 +69,16 @@ public abstract class AbstractAppInitiatedActionTest extends AbstractTestRealmKe
|
||||||
protected void assertRedirectSuccess() {
|
protected void assertRedirectSuccess() {
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void assertCancelMessage() {
|
||||||
|
String url = this.driver.getCurrentUrl();
|
||||||
|
Assert.assertTrue("Expected 'error=interaction_required' in url", url.contains("error=interaction_required"));
|
||||||
|
Assert.assertTrue("Expected 'error_description=User+cancelled+aplication-initiated+action.' in url", url.contains("error_description=User+cancelled+aplication-initiated+action."));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertSilentCancelMessage() {
|
||||||
|
String url = this.driver.getCurrentUrl();
|
||||||
|
Assert.assertFalse("Expected no 'error=' in url", url.contains("error="));
|
||||||
|
Assert.assertFalse("Expected no 'error_description=' in url", url.contains("error_description="));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ public class AppInitiatedActionCancelTest extends AbstractAppInitiatedActionTest
|
||||||
updateProfilePage.assertCurrent();
|
updateProfilePage.assertCurrent();
|
||||||
updateProfilePage.cancel();
|
updateProfilePage.cancel();
|
||||||
assertRedirectSuccess();
|
assertRedirectSuccess();
|
||||||
|
assertCancelMessage();
|
||||||
|
|
||||||
appPage.logout();
|
appPage.logout();
|
||||||
|
|
||||||
|
@ -58,4 +59,14 @@ public class AppInitiatedActionCancelTest extends AbstractAppInitiatedActionTest
|
||||||
loginPage.login("test-user@localhost", "password");
|
loginPage.login("test-user@localhost", "password");
|
||||||
updateProfilePage.assertCurrent();
|
updateProfilePage.assertCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void silentCancelUpdateProfile() {
|
||||||
|
doAIA(true);
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
updateProfilePage.assertCurrent();
|
||||||
|
updateProfilePage.cancel();
|
||||||
|
assertRedirectSuccess();
|
||||||
|
assertSilentCancelMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,7 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
|
||||||
changePasswordPage.cancel();
|
changePasswordPage.cancel();
|
||||||
|
|
||||||
assertRedirectSuccess();
|
assertRedirectSuccess();
|
||||||
|
assertCancelMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,12 @@
|
||||||
package org.keycloak.testsuite.actions;
|
package org.keycloak.testsuite.actions;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.models.AccountRoles;
|
import org.keycloak.models.AccountRoles;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -28,6 +30,9 @@ import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
*/
|
*/
|
||||||
public class AppInitiatedActionRoleTest extends AbstractAppInitiatedActionTest {
|
public class AppInitiatedActionRoleTest extends AbstractAppInitiatedActionTest {
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
public AppInitiatedActionRoleTest() {
|
public AppInitiatedActionRoleTest() {
|
||||||
super("update_profile");
|
super("update_profile");
|
||||||
}
|
}
|
||||||
|
@ -36,15 +41,15 @@ public class AppInitiatedActionRoleTest extends AbstractAppInitiatedActionTest {
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
List<RoleRepresentation> roleList = testRealm.getRoles().getClient().get("test-app");
|
List<RoleRepresentation> roleList = testRealm.getRoles().getClient().get("test-app");
|
||||||
|
|
||||||
RoleRepresentation initiateActionRole = null;
|
RoleRepresentation manageAccountRole = null;
|
||||||
for (RoleRepresentation role : roleList) {
|
for (RoleRepresentation role : roleList) {
|
||||||
if (role.getName().equals(AccountRoles.INITIATE_ACTION)) {
|
if (role.getName().equals(AccountRoles.MANAGE_ACCOUNT)) {
|
||||||
initiateActionRole = role;
|
manageAccountRole = role;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
roleList.remove(initiateActionRole);
|
roleList.remove(manageAccountRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -52,6 +57,6 @@ public class AppInitiatedActionRoleTest extends AbstractAppInitiatedActionTest {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
loginPage.login("test-user@localhost", "password");
|
loginPage.login("test-user@localhost", "password");
|
||||||
doAIA();
|
doAIA();
|
||||||
assertRedirectSuccess(); // update profile screen does not appear
|
errorPage.assertCurrent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,7 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
|
||||||
totpPage.cancel();
|
totpPage.cancel();
|
||||||
|
|
||||||
assertRedirectSuccess();
|
assertRedirectSuccess();
|
||||||
|
assertCancelMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -133,6 +133,7 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct
|
||||||
updateProfilePage.cancel();
|
updateProfilePage.cancel();
|
||||||
|
|
||||||
assertRedirectSuccess();
|
assertRedirectSuccess();
|
||||||
|
assertCancelMessage();
|
||||||
|
|
||||||
// assert nothing was updated in persistent store
|
// assert nothing was updated in persistent store
|
||||||
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
|
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
|
||||||
|
|
|
@ -123,7 +123,7 @@ public class AddUserTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RoleRepresentation> accountRoles = userResource.roles().clientLevel(accountId).listAll();
|
List<RoleRepresentation> accountRoles = userResource.roles().clientLevel(accountId).listAll();
|
||||||
assertRoles(accountRoles, "initiate-action", "view-profile", "manage-account");
|
assertRoles(accountRoles, "view-profile", "manage-account");
|
||||||
} finally {
|
} finally {
|
||||||
userResource.remove();
|
userResource.remove();
|
||||||
}
|
}
|
||||||
|
|
|
@ -438,7 +438,7 @@ public class ClientTest extends AbstractAdminTest {
|
||||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE);
|
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE);
|
||||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE);
|
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE);
|
||||||
|
|
||||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.INITIATE_ACTION, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS);
|
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS);
|
||||||
|
|
||||||
Assert.assertNames(scopesResource.getAll().getRealmMappings(), "role1");
|
Assert.assertNames(scopesResource.getAll().getRealmMappings(), "role1");
|
||||||
Assert.assertNames(scopesResource.getAll().getClientMappings().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings(), AccountRoles.VIEW_PROFILE);
|
Assert.assertNames(scopesResource.getAll().getClientMappings().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings(), AccountRoles.VIEW_PROFILE);
|
||||||
|
@ -453,7 +453,7 @@ public class ClientTest extends AbstractAdminTest {
|
||||||
Assert.assertNames(scopesResource.realmLevel().listEffective());
|
Assert.assertNames(scopesResource.realmLevel().listEffective());
|
||||||
Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "role1", "role2");
|
Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "role1", "role2");
|
||||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll());
|
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll());
|
||||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.INITIATE_ACTION, AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS);
|
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS);
|
||||||
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective());
|
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1409,7 +1409,7 @@ public class UserTest extends AbstractAdminTest {
|
||||||
assertNames(all.getRealmMappings(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
|
assertNames(all.getRealmMappings(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
|
||||||
assertEquals(2, all.getClientMappings().size());
|
assertEquals(2, all.getClientMappings().size());
|
||||||
assertNames(all.getClientMappings().get("myclient").getMappings(), "client-role", "client-composite");
|
assertNames(all.getClientMappings().get("myclient").getMappings(), "client-role", "client-composite");
|
||||||
assertNames(all.getClientMappings().get("account").getMappings(), "initiate-action", "manage-account", "view-profile");
|
assertNames(all.getClientMappings().get("account").getMappings(), "manage-account", "view-profile");
|
||||||
|
|
||||||
// Remove realm role
|
// Remove realm role
|
||||||
RoleRepresentation realmRoleRep = realm.roles().get("realm-role").toRepresentation();
|
RoleRepresentation realmRoleRep = realm.roles().get("realm-role").toRepresentation();
|
||||||
|
|
|
@ -129,7 +129,7 @@ public class KcAdmSessionTest extends AbstractAdmCliTest {
|
||||||
|
|
||||||
realmMappings = StreamSupport.stream(clientRoles.get("account").get("mappings").spliterator(), false)
|
realmMappings = StreamSupport.stream(clientRoles.get("account").get("mappings").spliterator(), false)
|
||||||
.map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
|
.map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
|
||||||
Assert.assertEquals(Arrays.asList("initiate-action", "manage-account", "view-profile"), realmMappings);
|
Assert.assertEquals(Arrays.asList("manage-account", "view-profile"), realmMappings);
|
||||||
|
|
||||||
realmMappings = StreamSupport.stream(clientRoles.get("realm-management").get("mappings").spliterator(), false)
|
realmMappings = StreamSupport.stream(clientRoles.get("realm-management").get("mappings").spliterator(), false)
|
||||||
.map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
|
.map(o -> o.get("name").asText()).sorted().collect(Collectors.toList());
|
||||||
|
|
|
@ -417,8 +417,8 @@
|
||||||
"client" : {
|
"client" : {
|
||||||
"test-app" : [
|
"test-app" : [
|
||||||
{
|
{
|
||||||
"name" : "initiate-action",
|
"name": "manage-account",
|
||||||
"description": "Allow application-initiated actions"
|
"description": "Allows application-initiated actions."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "customer-user",
|
"name": "customer-user",
|
||||||
|
|
|
@ -79,7 +79,6 @@ role_manage-clients=Manage clients
|
||||||
role_manage-events=Manage events
|
role_manage-events=Manage events
|
||||||
role_view-profile=View profile
|
role_view-profile=View profile
|
||||||
role_manage-account=Manage account
|
role_manage-account=Manage account
|
||||||
role_initiate-action=Initiate action
|
|
||||||
role_manage-account-links=Manage account links
|
role_manage-account-links=Manage account links
|
||||||
role_read-token=Read token
|
role_read-token=Read token
|
||||||
role_offline-access=Offline access
|
role_offline-access=Offline access
|
||||||
|
|
Loading…
Reference in a new issue