mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Select auth flow via acr using client policies (#36441)
Closes #24297 Co-authored-by: Ben Cresitello-Dittmar <bcresitellodittmar@mitre.org> Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
parent
dee203f320
commit
bd807ceac3
@ -14,3 +14,8 @@ an event.
|
||||
Now the Certificate Revocation Lists (CRL), that are used to validate certificates in the X.509 authenticator, are cached inside a new infinispan cache called `crl`. Caching improves the validation performance and decreases the memory consumption because just one CRL is maintained per source.
|
||||
|
||||
Check the `crl-storage` section in the link:https://www.keycloak.org/server/all-provider-config[All provider configuration] {section} to know the options for the new cache provider.
|
||||
|
||||
= Dynamic Authentication Flow selection using Client Policies
|
||||
|
||||
Introduced the ability to dynamically select authentication flows based on conditions such as requested scopes, ACR (Authentication Context Class Reference) and others.
|
||||
This can be achieved using link:{adminguide_link}#_client_policies[Client Policies] by combining the new `AuthenticationFlowSelectorExecutor` with conditions like the new `ACRCondition`. For more details, see the link:{adminguide_link}#_client-policy-auth-flow[{adminguide_name}].
|
||||
|
||||
@ -235,6 +235,17 @@ Creating an advanced flow such as this can have side effects. For example, if yo
|
||||
* In the *Action* menu, select *Bind flow* and select *Reset credentials flow* from the dropdown and click *Save*
|
||||
====
|
||||
|
||||
[[_client-policy-auth-flow]]
|
||||
==== Using Client Policies to Select an Authentication Flow
|
||||
<<_client_policies, Client Policies>> can be used to dynamically select an Authentication Flow based on specific conditions, such as requesting a particular scope or an ACR (Authentication Context Class Reference) using the `AuthenticationFlowSelectorExecutor` in combination with the condition you prefer.
|
||||
|
||||
The `AuthenticationFlowSelectorExecutor` allows you to select an appropriate authentication flow and set the level of authentication to be applied once the selected flow is completed.
|
||||
|
||||
A possible configuration involves using the `ACRCondition` in combination with the `AuthenticationFlowSelectorExecutor`. This setup enables you to select an authentication flow based on the requested ACR and have the ACR value included in the token using <<_mapping-acr-to-loa-realm,ACR to LoA Mapping>>.
|
||||
|
||||
For more details, see <<_client_policies, Client Policies>>.
|
||||
|
||||
|
||||
[[_step-up-flow]]
|
||||
==== Creating a browser login flow with step-up mechanism
|
||||
|
||||
@ -388,6 +399,13 @@ not be the desired behavior.
|
||||
|
||||
NOTE: A conflict situation may arise when an admin specifies several flows, sets different LoA levels to each, and assigns the flows to different clients. However, the rule is always the same: if a user has a certain level, it needs only have that level to connect to a client. It's up to the admin to make sure that the LoA is coherent.
|
||||
|
||||
NOTE: Step-up authentication with Level of Authentication conditions is intended for use cases where each level
|
||||
requires all authentication methods from the preceding levels.
|
||||
For instance, level X must always include all authentication methods required by level X-1.
|
||||
For use cases where a specific level, such as level 3, requires a different authentication method from the previous levels,
|
||||
it may be more appropriate to use mapping of ACR to a specific flow.
|
||||
For more details, see <<_client-policy-auth-flow, Using Client Policies to Select an Authentication Flow>>.
|
||||
|
||||
*Example scenario*
|
||||
|
||||
. Max Age is configured as 300 seconds for level 1 condition.
|
||||
|
||||
@ -94,6 +94,10 @@ Client Attribute::
|
||||
Any Client::
|
||||
This condition always evaluates to true. It can be used for example to ensure that all clients in the particular realm are FAPI compliant.
|
||||
|
||||
ACR Condition::
|
||||
Applied when an ACR value requested in the authentication request matches the value configured in the condition. For example, it can be used to select an authentication flow based on the requested ACR value. For more details, see the <<_client-policy-auth-flow, related documentation>> and the https://openid.net/specs/openid-connect-core-1_0.html#acrSemantics[official OIDC specification].
|
||||
|
||||
|
||||
=== Executor
|
||||
|
||||
An executor specifies what action is executed on a client to which a policy is adopted. The executor executes one or several specified actions. For example,
|
||||
@ -143,6 +147,8 @@ One of several purposes for this executor is to realize the security requirement
|
||||
* Enforce a valid redirect URI that the OAuth 2.1 specification requires
|
||||
* Enforce SAML Redirect binding cannot be used or SAML requests and assertions are signed
|
||||
|
||||
Another available executor is the `auth-flow-enforce`, which can be used to enforce an authentication flow during an authentication request. For instance, it can be used to select a flow based on certain conditions, such as a specific scope or an ACR value. For more details, see the <<_client-policy-auth-flow, related documentation>>.
|
||||
|
||||
[[_client_policy_profile]]
|
||||
=== Profile
|
||||
|
||||
|
||||
@ -190,4 +190,13 @@ public final class Constants {
|
||||
public static final String IS_TEMP_ADMIN_ATTR_NAME = "is_temporary_admin";
|
||||
|
||||
public static final String ADMIN_PERMISSIONS_CLIENT_ID = "admin-permissions";
|
||||
|
||||
// Note used to store the authentication flow requested
|
||||
public static final String REQUESTED_AUTHENTICATION_FLOW = "requested-authentication-flow";
|
||||
|
||||
public static final String AUTHENTICATION_FLOW_LEVEL_OF_AUTHENTICATION = "authentication-flow-level-of-authentication";
|
||||
|
||||
// Note used to store the acr values if it is matched by client policy condition
|
||||
public static final String CLIENT_POLICY_REQUESTED_ACR = "client-policy-requested-acr";
|
||||
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.AuthenticationFlowBindings;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
/**
|
||||
@ -33,6 +35,19 @@ public class AuthenticationFlowResolver {
|
||||
public static AuthenticationFlowModel resolveBrowserFlow(AuthenticationSessionModel authSession) {
|
||||
AuthenticationFlowModel flow = null;
|
||||
ClientModel client = authSession.getClient();
|
||||
|
||||
// check if specific flow has been requested
|
||||
String requestedFlowAlias = authSession.getAuthNote(Constants.REQUESTED_AUTHENTICATION_FLOW);
|
||||
if (requestedFlowAlias != null){
|
||||
flow = authSession.getRealm().getFlowByAlias(requestedFlowAlias);
|
||||
// validate flow exists
|
||||
if (flow == null){
|
||||
throw new ModelException("Client " + client.getClientId() + " has requested browser flow " + requestedFlowAlias + ", but this flow does not exist.");
|
||||
} else {
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
|
||||
String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
|
||||
if (clientFlow != null) {
|
||||
flow = authSession.getRealm().getAuthenticationFlowById(clientFlow);
|
||||
@ -47,6 +62,19 @@ public class AuthenticationFlowResolver {
|
||||
public static AuthenticationFlowModel resolveDirectGrantFlow(AuthenticationSessionModel authSession) {
|
||||
AuthenticationFlowModel flow = null;
|
||||
ClientModel client = authSession.getClient();
|
||||
|
||||
// check if specific flow has been requested
|
||||
String requestedFlowAlias = authSession.getAuthNote(Constants.REQUESTED_AUTHENTICATION_FLOW);
|
||||
if (requestedFlowAlias != null){
|
||||
flow = authSession.getRealm().getFlowByAlias(requestedFlowAlias);
|
||||
// validate flow exists
|
||||
if (flow == null){
|
||||
throw new ModelException("Client " + client.getClientId() + " has requested browser flow " + requestedFlowAlias + ", but this flow does not exist.");
|
||||
} else {
|
||||
return flow;
|
||||
}
|
||||
}
|
||||
|
||||
String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING);
|
||||
if (clientFlow != null) {
|
||||
flow = authSession.getRealm().getAuthenticationFlowById(clientFlow);
|
||||
|
||||
@ -19,6 +19,7 @@ package org.keycloak.authentication;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.authenticators.util.AcrStore;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
|
||||
@ -1185,6 +1186,8 @@ public class AuthenticationProcessor {
|
||||
}
|
||||
|
||||
protected Response authenticationComplete() {
|
||||
new AcrStore(session, authenticationSession).setAuthFlowLevelAuthenticatedToCurrentRequest();
|
||||
|
||||
// attachSession(); // Session will be attached after requiredActions + consents are finished.
|
||||
AuthenticationManager.setClientScopesInSession(session, authenticationSession);
|
||||
|
||||
|
||||
@ -188,6 +188,17 @@ public class AcrStore {
|
||||
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(level));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set level to the current authentication session if an auth flow loa is present and is higher then the current loa
|
||||
*/
|
||||
public void setAuthFlowLevelAuthenticatedToCurrentRequest() {
|
||||
if (authSession.getAuthNote(Constants.AUTHENTICATION_FLOW_LEVEL_OF_AUTHENTICATION) != null) {
|
||||
int authFlowLoa = Integer.parseInt(authSession.getAuthNote(Constants.AUTHENTICATION_FLOW_LEVEL_OF_AUTHENTICATION));
|
||||
if (getLevelOfAuthenticationFromCurrentAuthentication() < authFlowLoa) {
|
||||
setLevelAuthenticatedToCurrentRequest(authFlowLoa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setLevelAuthenticatedToMap(int level) {
|
||||
Map<Integer, Integer> levels = getCurrentAuthenticatedLevelsMap();
|
||||
|
||||
@ -47,6 +47,7 @@ import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
|
||||
import org.keycloak.services.clientpolicy.context.PreAuthorizationRequestContext;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
@ -193,13 +194,15 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
request.setDpopJkt(dpopJkt);
|
||||
}
|
||||
|
||||
authenticationSession = createAuthenticationSession(client, request.getState());
|
||||
|
||||
try {
|
||||
session.clientPolicy().triggerOnEvent(new AuthorizationRequestContext(parsedResponseType, request, redirectUri, params));
|
||||
session.clientPolicy().triggerOnEvent(new AuthorizationRequestContext(parsedResponseType, request, redirectUri, params, authenticationSession));
|
||||
} catch (ClientPolicyException cpe) {
|
||||
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authenticationSession, false);
|
||||
return redirectErrorToClient(parsedResponseMode, cpe.getError(), cpe.getErrorDetail());
|
||||
}
|
||||
|
||||
authenticationSession = createAuthenticationSession(client, request.getState());
|
||||
updateAuthenticationSession();
|
||||
|
||||
// So back button doesn't work
|
||||
@ -366,6 +369,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||
}
|
||||
}).min().ifPresent(loa -> authenticationSession.setClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION, String.valueOf(loa)));
|
||||
|
||||
|
||||
if (request.getAdditionalReqParams() != null) {
|
||||
for (String paramName : request.getAdditionalReqParams().keySet()) {
|
||||
authenticationSession.setClientNote(LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2021 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.services.clientpolicy.condition;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.utils.AcrUtils;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyVote;
|
||||
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:ggrazian@redhat.com">Giuseppe Graziano</a>
|
||||
*/
|
||||
public class AcrCondition extends AbstractClientPolicyConditionProvider<AcrCondition.Configuration> {
|
||||
|
||||
public AcrCondition(KeycloakSession session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
public static class Configuration extends ClientPolicyConditionConfigurationRepresentation {
|
||||
|
||||
@JsonProperty("acr-property")
|
||||
protected String acrProperty;
|
||||
|
||||
public String getAcrProperty() {
|
||||
return acrProperty;
|
||||
}
|
||||
|
||||
public void setAcrProperty(String acrProperty) {
|
||||
this.acrProperty = acrProperty;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Configuration> getConditionConfigurationClass() {
|
||||
return Configuration.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return AnyClientConditionFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPolicyException {
|
||||
if (context.getEvent() == ClientPolicyEvent.AUTHORIZATION_REQUEST) {
|
||||
AuthorizationRequestContext authorizationRequestContext = ((AuthorizationRequestContext) context);
|
||||
if (containsAcr(authorizationRequestContext)) {
|
||||
authorizationRequestContext.getAuthenticationSession().setAuthNote(Constants.CLIENT_POLICY_REQUESTED_ACR, configuration.getAcrProperty());
|
||||
return ClientPolicyVote.YES;
|
||||
}
|
||||
else {
|
||||
return ClientPolicyVote.NO;
|
||||
}
|
||||
}
|
||||
return ClientPolicyVote.ABSTAIN;
|
||||
}
|
||||
|
||||
private boolean containsAcr(AuthorizationRequestContext context) {
|
||||
List<String> acrValues = AcrUtils.getAcrValues(context.getAuthorizationEndpointRequest().getClaims(), context.getAuthorizationEndpointRequest().getAcr(), session.getContext().getClient());
|
||||
return acrValues != null && !acrValues.isEmpty() && acrValues.contains(configuration.getAcrProperty());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2021 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.services.clientpolicy.condition;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:ggrazian@redhat.com">Giuseppe Graziano</a>
|
||||
*/
|
||||
public class AcrConditionFactory extends AbstractClientPolicyConditionProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "acr-condition";
|
||||
|
||||
public static final String ACR_PROPERTY = "acr-property";
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property = new ProviderConfigProperty(ACR_PROPERTY, "ACR",
|
||||
"ACR to be requested to satisfy the condition",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientPolicyConditionProvider create(KeycloakSession session) {
|
||||
return new AcrCondition(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "The condition is satisfied when configured acr value is requested";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,7 @@ import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
@ -35,14 +36,18 @@ public class AuthorizationRequestContext implements ClientPolicyContext {
|
||||
private final String redirectUri;
|
||||
private final MultivaluedMap<String, String> requestParameters;
|
||||
|
||||
private final AuthenticationSessionModel authenticationSession;
|
||||
|
||||
public AuthorizationRequestContext(OIDCResponseType parsedResponseType,
|
||||
AuthorizationEndpointRequest request,
|
||||
String redirectUri,
|
||||
MultivaluedMap<String, String> requestParameters) {
|
||||
AuthorizationEndpointRequest request,
|
||||
String redirectUri,
|
||||
MultivaluedMap<String, String> requestParameters,
|
||||
AuthenticationSessionModel authenticationSession) {
|
||||
this.parsedResponseType = parsedResponseType;
|
||||
this.request = request;
|
||||
this.redirectUri = redirectUri;
|
||||
this.requestParameters = requestParameters;
|
||||
this.authenticationSession = authenticationSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,4 +74,8 @@ public class AuthorizationRequestContext implements ClientPolicyContext {
|
||||
public boolean isParRequest() {
|
||||
return requestParameters.containsKey(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||
}
|
||||
|
||||
public AuthenticationSessionModel getAuthenticationSession() {
|
||||
return authenticationSession;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2021 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.services.clientpolicy.executor;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.authentication.authenticators.util.AcrStore;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:ggrazian@redhat.com">Giuseppe Graziano</a>
|
||||
*/
|
||||
public class AuthenticationFlowSelectorExecutor implements ClientPolicyExecutorProvider<AuthenticationFlowSelectorExecutor.Configuration> {
|
||||
|
||||
private Configuration configuration;
|
||||
|
||||
public AuthenticationFlowSelectorExecutor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupConfiguration(Configuration config) {
|
||||
this.configuration = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Configuration> getExecutorConfigurationClass() {
|
||||
return Configuration.class;
|
||||
}
|
||||
|
||||
public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation {
|
||||
@JsonProperty("auth-flow-alias")
|
||||
protected String authFlowAlias;
|
||||
|
||||
@JsonProperty("auth-flow-loa")
|
||||
protected Integer authFlowLoa;
|
||||
|
||||
public String getAuthFlowAlias() {
|
||||
return authFlowAlias;
|
||||
}
|
||||
|
||||
public void setAuthFlowAlias(String authFlowAlias) {
|
||||
this.authFlowAlias = authFlowAlias;
|
||||
}
|
||||
|
||||
public Integer getAuthFlowLoa() {
|
||||
return authFlowLoa;
|
||||
}
|
||||
|
||||
public void setAuthFlowLoa(Integer authFlowLoa) {
|
||||
this.authFlowLoa = authFlowLoa;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return PKCEEnforcerExecutorFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
|
||||
if (context.getEvent() == ClientPolicyEvent.AUTHORIZATION_REQUEST) {
|
||||
AuthorizationRequestContext authorizationRequestContext = (AuthorizationRequestContext) context;
|
||||
executeOnAuthorizationRequest(authorizationRequestContext.getAuthenticationSession());
|
||||
}
|
||||
}
|
||||
|
||||
private void executeOnAuthorizationRequest(AuthenticationSessionModel authSession) {
|
||||
if (configuration.getAuthFlowAlias() != null) {
|
||||
authSession.setAuthNote(Constants.REQUESTED_AUTHENTICATION_FLOW, configuration.getAuthFlowAlias());
|
||||
// auth flow selected via acr condition
|
||||
if (configuration.getAuthFlowLoa() != null) {
|
||||
authSession.setAuthNote(Constants.AUTHENTICATION_FLOW_LEVEL_OF_AUTHENTICATION, String.valueOf(configuration.getAuthFlowLoa()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2021 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.services.clientpolicy.executor;
|
||||
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:ggrazian@redhat.com">Giuseppe Graziano</a>
|
||||
*/
|
||||
public class AuthenticationFlowSelectorExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "auth-flow-enforcer";
|
||||
|
||||
public static final String AUTH_FLOW_ALIAS = "auth-flow-alias";
|
||||
|
||||
private static final ProviderConfigProperty AUTH_FLOW_ALIAS_PROPERTY = new ProviderConfigProperty(
|
||||
AUTH_FLOW_ALIAS, "Auth Flow Alias", "Insert the alias of the authentication flow",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
|
||||
private static final ProviderConfigProperty AUTH_FLOW_LOA_PROPERTY = new ProviderConfigProperty(
|
||||
AUTH_FLOW_ALIAS, "Auth Flow Loa", "Insert the loa to enforce when the selected authentication flow is executed",
|
||||
ProviderConfigProperty.INTEGER_TYPE, 1);
|
||||
|
||||
@Override
|
||||
public AuthenticationFlowSelectorExecutor create(KeycloakSession session) {
|
||||
return new AuthenticationFlowSelectorExecutor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Arrays.asList(AUTH_FLOW_ALIAS_PROPERTY, AUTH_FLOW_LOA_PROPERTY);
|
||||
}
|
||||
|
||||
}
|
||||
@ -8,3 +8,4 @@ org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesConditionFa
|
||||
org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory
|
||||
org.keycloak.services.clientpolicy.condition.ClientProtocolConditionFactory
|
||||
org.keycloak.services.clientpolicy.condition.ClientAttributesConditionFactory
|
||||
org.keycloak.services.clientpolicy.condition.AcrConditionFactory
|
||||
|
||||
@ -28,3 +28,4 @@ org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFa
|
||||
org.keycloak.services.clientpolicy.executor.SamlSecureClientUrisExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.SamlAvoidRedirectBindingExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.SamlSignatureEnforcerExecutorFactory
|
||||
org.keycloak.services.clientpolicy.executor.AuthenticationFlowSelectorExecutorFactory
|
||||
|
||||
@ -0,0 +1,510 @@
|
||||
/*
|
||||
* Copyright 2021 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.testsuite.oidc;
|
||||
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.Assert;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.representations.ClaimsRepresentation;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.clientpolicy.condition.AcrCondition;
|
||||
import org.keycloak.services.clientpolicy.condition.AcrConditionFactory;
|
||||
import org.keycloak.services.clientpolicy.executor.AuthenticationFlowSelectorExecutor;
|
||||
import org.keycloak.services.clientpolicy.executor.AuthenticationFlowSelectorExecutorFactory;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.LoginTotpPage;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:ggrazian@redhat.com">Giuseppe Graziano</a>
|
||||
*/
|
||||
public class AcrAuthFlowTest extends AbstractOIDCScopeTest{
|
||||
|
||||
// config
|
||||
private static String CLIENT_ID = "test-app";
|
||||
private static String CLIENT_SECRET = "password";
|
||||
private static String PASSWORD = "password";
|
||||
private static String TOTP_SECRET = "totpsecret";
|
||||
|
||||
private static String PASSWORD_FLOW_ALIAS = "password-flow";
|
||||
|
||||
private static String PASSWORD_OTP_FLOW_ALIAS = "password-otp-flow";
|
||||
|
||||
// pages
|
||||
@Page
|
||||
protected LoginTotpPage loginTotpPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
private TimeBasedOTP totp = new TimeBasedOTP();
|
||||
private static String userId;
|
||||
|
||||
/**
|
||||
* Create the ACR protocol mapper and add it to the test OIDC client.
|
||||
* @param testRealm The realm read from /testrealm.json.
|
||||
*/
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
// setup user
|
||||
UserRepresentation user = createTestUser("test-user", PASSWORD, TOTP_SECRET);
|
||||
testRealm.getUsers().add(user);
|
||||
userId = user.getId();
|
||||
|
||||
// setup acr scope
|
||||
ClientScopeRepresentation scope = createScope();
|
||||
|
||||
List<ClientScopeRepresentation> scopes = testRealm.getClientScopes();
|
||||
if (scopes == null){
|
||||
testRealm.setClientScopes(new ArrayList<>());
|
||||
}
|
||||
testRealm.getClientScopes().add(scope);
|
||||
|
||||
// update client and default scopes
|
||||
testRealm.setDefaultDefaultClientScopes(Collections.singletonList(scope.getName()));
|
||||
testRealm.getClients().stream().filter(c -> c.getClientId().equals(CLIENT_ID)).findFirst().orElseThrow().setDefaultClientScopes(Collections.singletonList(scope.getName()));
|
||||
|
||||
try {
|
||||
findTestApp(testRealm).setAttributes(Collections.singletonMap(Constants.ACR_LOA_MAP, getAcrToLoaMappingForClient()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getAcrToLoaMappingForClient() throws IOException {
|
||||
Map<String, Integer> acrLoaMap = new HashMap<>();
|
||||
acrLoaMap.put("default", 1);
|
||||
acrLoaMap.put("acr-password", 2);
|
||||
acrLoaMap.put("acr-otp", 3);
|
||||
return JsonSerialization.writeValueAsString(acrLoaMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a test user, optionally with OTP configured
|
||||
* @param username The username of the user to create
|
||||
* @param password The password to set on the user
|
||||
* @param totpSecret If set, will configure a totp authenticator with this secret
|
||||
* @return
|
||||
*/
|
||||
private UserRepresentation createTestUser(String username, String password, String totpSecret){
|
||||
UserBuilder builder = UserBuilder.create()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.username(username)
|
||||
.enabled(true)
|
||||
.email(username + "@email.com")
|
||||
.firstName(username)
|
||||
.lastName(username)
|
||||
.password(password);
|
||||
|
||||
if (totpSecret != null){
|
||||
builder.totpSecret(totpSecret)
|
||||
.otpEnabled();
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create the ACR scope and protocol mapper.
|
||||
* @return The created scope object
|
||||
*/
|
||||
private ClientScopeRepresentation createScope(){
|
||||
ProtocolMapperRepresentation protocolMapper = createMapper();
|
||||
return new ClientScopeRepresentation(){{
|
||||
setId(KeycloakModelUtils.generateId());
|
||||
setName("acr-test-scope");
|
||||
setProtocol("openid-connect");
|
||||
setAttributes(new HashMap<>() {{
|
||||
put(ClientScopeModel.INCLUDE_IN_TOKEN_SCOPE, "false");
|
||||
put(ClientScopeModel.DISPLAY_ON_CONSENT_SCREEN, "false");
|
||||
}});
|
||||
setProtocolMappers(Collections.singletonList(protocolMapper));
|
||||
}};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create the acr protocol mapper.
|
||||
* @return The created protocol mapper
|
||||
*/
|
||||
private ProtocolMapperRepresentation createMapper(){
|
||||
return new ProtocolMapperRepresentation(){{
|
||||
setId(KeycloakModelUtils.generateId());
|
||||
setName("acr-test-mapper");
|
||||
setProtocol("openid-connect");
|
||||
setProtocolMapper("oidc-acr-mapper");
|
||||
setConfig(new HashMap<>() {{
|
||||
put("id.token.claim", "true");
|
||||
put("access.token.claim", "true");
|
||||
}});
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup for the test cases
|
||||
*/
|
||||
@Before
|
||||
public void setupTest() {
|
||||
oauth.clientId(CLIENT_ID);
|
||||
createPasswordFlow();
|
||||
createOTPFlow();
|
||||
|
||||
// needed otherwise multiple OTP tests will fail due to token reuse
|
||||
new RealmAttributeUpdater(testRealm())
|
||||
.setOtpPolicyCodeReusable(true)
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset clients post test
|
||||
*/
|
||||
@After
|
||||
public void cleanupTest() {
|
||||
try {
|
||||
ClientPoliciesRepresentation clientPolicies = JsonSerialization.readValue("{}", ClientPoliciesRepresentation.class);
|
||||
adminClient.realm(TEST_REALM_NAME).clientPoliciesPoliciesResource().updatePolicies(clientPolicies);
|
||||
|
||||
ClientProfilesRepresentation clientProfilesRepresentation = JsonSerialization.readValue("{}", ClientProfilesRepresentation.class);
|
||||
|
||||
adminClient.realm(TEST_REALM_NAME).clientPoliciesProfilesResource().updateProfiles(clientProfilesRepresentation);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Assert.fail();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an authentication flow with the password authenticator
|
||||
*/
|
||||
private void createPasswordFlow(){
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(PASSWORD_FLOW_ALIAS));
|
||||
testingClient.server(TEST_REALM_NAME)
|
||||
.run(session -> FlowUtil.inCurrentRealm(session).selectFlow(PASSWORD_FLOW_ALIAS)
|
||||
// remove cookie, kerberos, and idp from browser flow
|
||||
.removeExecution(2).removeExecution(1).removeExecution(0)
|
||||
.inForms(forms -> forms.clear()
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID, null)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an authentication flow with the password and otp authenticators
|
||||
*/
|
||||
private void createOTPFlow(){
|
||||
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(PASSWORD_OTP_FLOW_ALIAS));
|
||||
testingClient.server(TEST_REALM_NAME)
|
||||
.run(session -> FlowUtil.inCurrentRealm(session).selectFlow(PASSWORD_OTP_FLOW_ALIAS)
|
||||
// remove cookie, kerberos, and idp from browser flow
|
||||
.removeExecution(2).removeExecution(1).removeExecution(0)
|
||||
.inForms(forms -> forms.clear()
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernamePasswordFormFactory.PROVIDER_ID, null)
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, OTPFormAuthenticatorFactory.PROVIDER_ID, null)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the ACR auth flow map for the password auth flow
|
||||
* Expected: ACR = "acr-password"
|
||||
*/
|
||||
@Test
|
||||
public void testAuthFlow() {
|
||||
setAcrClientPolicy(adminClient, TEST_REALM_NAME, "acr-password", PASSWORD_FLOW_ALIAS, 2);
|
||||
setAcrClientPolicy(adminClient, TEST_REALM_NAME, "acr-otp", PASSWORD_OTP_FLOW_ALIAS, 3);
|
||||
|
||||
loginWithAcr(new ArrayList<>(){{
|
||||
add("acr-password");
|
||||
}});
|
||||
|
||||
authenticatePassword("test-user", PASSWORD);
|
||||
Tokens tokens = assertLoginWithAcr(userId, "acr-password");
|
||||
|
||||
logout(userId, tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthFlowWithoutLoaConfig() {
|
||||
setAcrClientPolicy(adminClient, TEST_REALM_NAME, "acr-password", PASSWORD_FLOW_ALIAS);
|
||||
|
||||
loginWithAcr(new ArrayList<>(){{
|
||||
add("acr-password");
|
||||
}});
|
||||
|
||||
authenticatePassword("test-user", PASSWORD);
|
||||
Tokens tokens = assertLoginWithAcr(userId, "default");
|
||||
|
||||
logout(userId, tokens);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the ACR auth flow mapping feature for an alternate otp auth flow
|
||||
* Expected: ACR = "acr-otp"
|
||||
*/
|
||||
@Test
|
||||
public void testAuthFlowOTP() {
|
||||
|
||||
setAcrClientPolicy(adminClient, TEST_REALM_NAME, "acr-password", PASSWORD_FLOW_ALIAS, 2);
|
||||
setAcrClientPolicy(adminClient, TEST_REALM_NAME, "acr-otp", PASSWORD_OTP_FLOW_ALIAS, 3);
|
||||
|
||||
loginWithAcr(new ArrayList<>(){{
|
||||
add("acr-otp");
|
||||
}});
|
||||
|
||||
authenticatePassword("test-user", PASSWORD);
|
||||
authenticateTOTP(TOTP_SECRET);
|
||||
Tokens tokens = assertLoginWithAcr(userId, "acr-otp");
|
||||
|
||||
logout(userId, tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test fallback to default flow when no valid mapping is found. Ensure acr is default value 1
|
||||
* Expected: ACR = default with the default acr-loa mapping behavior
|
||||
*/
|
||||
@Test
|
||||
public void testNoMapping() {
|
||||
|
||||
loginWithAcr(new ArrayList<>(){{
|
||||
add("acr-password");
|
||||
}});
|
||||
|
||||
authenticatePassword("test-user", PASSWORD);
|
||||
authenticateTOTP(TOTP_SECRET);
|
||||
Tokens tokens = assertLoginWithAcr(userId, "default");
|
||||
|
||||
logout(userId, tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test sessions when using ACR flow mapping
|
||||
*
|
||||
* Expected: Re-authentication forces user to redo authenticators for newly specified flow
|
||||
*/
|
||||
@Test
|
||||
public void testSessionReAuth() {
|
||||
Tokens tokens;
|
||||
|
||||
setAcrClientPolicy(adminClient, TEST_REALM_NAME, "acr-password", PASSWORD_FLOW_ALIAS, 2);
|
||||
setAcrClientPolicy(adminClient, TEST_REALM_NAME, "acr-otp", PASSWORD_OTP_FLOW_ALIAS, 3);
|
||||
|
||||
// initial login
|
||||
loginWithAcr(new ArrayList<>(){{
|
||||
add("acr-password");
|
||||
}});
|
||||
authenticatePassword("test-user", PASSWORD);
|
||||
assertLoginWithAcr(userId, "acr-password");
|
||||
|
||||
// ensure re-auth forced with different acr
|
||||
loginWithAcr(new ArrayList<>(){{
|
||||
add("acr-otp");
|
||||
}});
|
||||
authenticatePassword("test-user", PASSWORD);
|
||||
authenticateTOTP(TOTP_SECRET);
|
||||
tokens = assertLoginWithAcr(userId, "acr-otp");
|
||||
|
||||
logout(userId, tokens);
|
||||
}
|
||||
|
||||
private void setAcrClientPolicy(Keycloak adminClient, String realm, String acr, String alias) {
|
||||
setAcrClientPolicy(adminClient, realm, acr, alias, null);
|
||||
}
|
||||
|
||||
public static void setAcrClientPolicy(Keycloak adminClient, String realm, String acr, String alias, Integer loa) {
|
||||
|
||||
try {
|
||||
|
||||
ClientProfilesRepresentation clientProfiles = adminClient.realm(realm).clientPoliciesProfilesResource().getProfiles(false);
|
||||
AuthenticationFlowSelectorExecutor.Configuration aliasConfiguration = new AuthenticationFlowSelectorExecutor.Configuration();
|
||||
aliasConfiguration.setAuthFlowAlias(alias);
|
||||
if (loa != null) {
|
||||
aliasConfiguration.setAuthFlowLoa(loa);
|
||||
}
|
||||
ClientPoliciesUtil.ClientProfilesBuilder clientProfilesBuilder = new ClientPoliciesUtil.ClientProfilesBuilder()
|
||||
.addProfile(
|
||||
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(alias, "")
|
||||
.addExecutor(AuthenticationFlowSelectorExecutorFactory.PROVIDER_ID, aliasConfiguration)
|
||||
.toRepresentation()
|
||||
);
|
||||
clientProfiles.getProfiles().forEach(clientProfilesBuilder::addProfile);
|
||||
String json = clientProfilesBuilder.toString();
|
||||
|
||||
clientProfiles = JsonSerialization.readValue(json, ClientProfilesRepresentation.class);
|
||||
adminClient.realm(realm).clientPoliciesProfilesResource().updateProfiles(clientProfiles);
|
||||
|
||||
|
||||
ClientPoliciesRepresentation clientPolicies = adminClient.realm(realm).clientPoliciesPoliciesResource().getPolicies(false);
|
||||
|
||||
AcrCondition.Configuration acrConfiguration = new AcrCondition.Configuration();
|
||||
acrConfiguration.setAcrProperty(acr);
|
||||
|
||||
// register policies
|
||||
ClientPoliciesUtil.ClientPoliciesBuilder clientPoliciesBuilder = new ClientPoliciesUtil.ClientPoliciesBuilder()
|
||||
.addPolicy(
|
||||
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(alias, "", Boolean.TRUE)
|
||||
.addCondition(AcrConditionFactory.PROVIDER_ID,
|
||||
acrConfiguration)
|
||||
.addProfile(alias)
|
||||
.toRepresentation()
|
||||
);
|
||||
|
||||
clientPolicies.getPolicies().forEach(clientPoliciesBuilder::addPolicy);
|
||||
json = clientPoliciesBuilder.toString();
|
||||
|
||||
clientPolicies = json==null ? null : JsonSerialization.readValue(json, ClientPoliciesRepresentation.class);
|
||||
adminClient.realm(realm).clientPoliciesPoliciesResource().updatePolicies(clientPolicies);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Assert.fail();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void loginWithAcr(List<String> acrValues){
|
||||
loginWithAcr(acrValues, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to open the authentication page, requesting the specified acrValues. Optionally, specify the acr
|
||||
* claim as essential.
|
||||
* @param acrValues The acr values to include in the authorization request
|
||||
* @param essential Specify that the acr claim is essential in the request
|
||||
*/
|
||||
private void loginWithAcr(List<String> acrValues, boolean essential){
|
||||
ClaimsRepresentation.ClaimValue<String> acrClaim = new ClaimsRepresentation.ClaimValue<>();
|
||||
acrClaim.setEssential(essential);
|
||||
acrClaim.setValues(acrValues);
|
||||
|
||||
ClaimsRepresentation claims = new ClaimsRepresentation();
|
||||
claims.setIdTokenClaims(Collections.singletonMap(IDToken.ACR, acrClaim));
|
||||
|
||||
oauth.claims(claims);
|
||||
oauth.openLoginForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to fetch the authentication flow ID based on the alias
|
||||
* @param alias The alias to search for
|
||||
* @return The flow ID
|
||||
*/
|
||||
private String findFlowByAlias(String alias){
|
||||
return testRealm().flows().getFlows().stream().filter(f -> f.getAlias().equals(alias)).findFirst().orElseThrow().getId();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to log out the specified user
|
||||
* @param userId The keycloak identifier of the user
|
||||
* @param tokens The OIDC tokens received during login
|
||||
*/
|
||||
private void logout(String userId, Tokens tokens){
|
||||
// Logout
|
||||
oauth.doLogout(tokens.refreshToken, CLIENT_SECRET);
|
||||
events.expectLogout(tokens.idToken.getSessionState())
|
||||
.client(CLIENT_ID)
|
||||
.user(userId)
|
||||
.removeDetail(Details.REDIRECT_URI).assertEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to authenticate with a username and password
|
||||
* @param username The username to log in with
|
||||
* @param password The password to log in with
|
||||
*/
|
||||
private void authenticatePassword(String username, String password){
|
||||
Assert.assertTrue(loginPage.isCurrent());
|
||||
loginPage.login(username, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to authenticate with a TOTP token
|
||||
* @param totpSecret The secret to use to generate the TOTP token
|
||||
*/
|
||||
private void authenticateTOTP(String totpSecret){
|
||||
Assert.assertTrue(loginTotpPage.isCurrent());
|
||||
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
|
||||
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to assert login completed successfully for the specified user
|
||||
* @param userId The keycloak ID of the user to check
|
||||
* @param expectedAcr The value expected in the 'acr' claim of the resulting token
|
||||
* @return The tokens from a successful login
|
||||
*/
|
||||
private Tokens assertLoginWithAcr(String userId, String expectedAcr){
|
||||
EventRepresentation loginEvent = events.expectLogin()
|
||||
.user(userId)
|
||||
.assertEvent();
|
||||
|
||||
Tokens tokens = sendTokenRequest(loginEvent, userId, "openid", CLIENT_ID);
|
||||
assertAcr(tokens.idToken, expectedAcr);
|
||||
assertAcr(tokens.accessToken, expectedAcr);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to assert the token contains the specified acr value
|
||||
* @param token The token to check (either access or ID)
|
||||
* @param expectedAcr The expected acr values in the token
|
||||
*/
|
||||
private void assertAcr(IDToken token, String expectedAcr) {
|
||||
getLogger().infof("Expected acr = %s", expectedAcr);
|
||||
String acr = token.getAcr();
|
||||
getLogger().infof("Response acr = %s", acr);
|
||||
if (expectedAcr != null) {
|
||||
Assert.assertNotNull(acr);
|
||||
}
|
||||
|
||||
Assert.assertEquals(acr, expectedAcr);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user