Skip to content

Commit

Permalink
Select auth flow via acr using client policies (keycloak#36441)
Browse files Browse the repository at this point in the history
Closes keycloak#24297


Co-authored-by: Ben Cresitello-Dittmar <[email protected]>
Signed-off-by: Giuseppe Graziano <[email protected]>
  • Loading branch information
graziang and Ben Cresitello-Dittmar authored Jan 23, 2025
1 parent dee203f commit bd807ce
Show file tree
Hide file tree
Showing 16 changed files with 938 additions and 5 deletions.
5 changes: 5 additions & 0 deletions docs/documentation/release_notes/topics/26_2_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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}].
18 changes: 18 additions & 0 deletions docs/documentation/server_admin/topics/authentication/flows.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
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;

/**
Expand All @@ -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);
Expand All @@ -47,6 +62,19 @@ public static AuthenticationFlowModel resolveBrowserFlow(AuthenticationSessionMo
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

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;
Expand Down Expand Up @@ -1185,6 +1186,8 @@ public void validateUser(UserModel authenticatedUser) {
}

protected Response authenticationComplete() {
new AcrStore(session, authenticationSession).setAuthFlowLevelAuthenticatedToCurrentRequest();

// attachSession(); // Session will be attached after requiredActions + consents are finished.
AuthenticationManager.setClientScopesInSession(session, authenticationSession);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,17 @@ public void setLevelAuthenticatedToCurrentRequest(int level) {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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;
Expand Down Expand Up @@ -193,13 +194,15 @@ private Response process(final MultivaluedMap<String, String> params) {
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
Expand Down Expand Up @@ -366,6 +369,7 @@ private void updateAuthenticationSession() {
}
}).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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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:[email protected]">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());
}

}
Original file line number Diff line number Diff line change
@@ -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:[email protected]">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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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:[email protected]">Takashi Norimatsu</a>
Expand All @@ -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
Expand All @@ -69,4 +74,8 @@ public MultivaluedMap<String, String> getRequestParameters() {
public boolean isParRequest() {
return requestParameters.containsKey(OIDCLoginProtocol.REQUEST_URI_PARAM);
}

public AuthenticationSessionModel getAuthenticationSession() {
return authenticationSession;
}
}
Loading

0 comments on commit bd807ce

Please sign in to comment.