Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,11 @@ a `claims` parameter that has an `acr` claim attached. See https://openid.net/sp

WARNING: Note that default ACR values are used as the default level, however it cannot be reliably used to enforce login with the particular level.
For example, assume that you configure the `Default ACR Values` to level 2. Then by default, users will be required to authenticate with level 2.
However when the user explicitly attaches the parameter into login request such as `acr_values=1`, then the level 1 will be used. As a result, if the client
However, when the user explicitly attaches the parameter into login request such as `acr_values=1`, then the level 1 will be used. As a result, if the client
really requires level 2, the client is encouraged to check the presence of the `acr` claim inside ID Token and double-check that it contains the requested level 2.
To actually enforce the usage of a certain ACR on the {project_name} side, use the `Minimum ACR Value` setting.
This allows administrators to enforce ACRs even on applications that are not able to validate the requested `acr` claim inside the token.


image:images/client-oidc-map-acr-to-loa.png[alt="ACR to LoA mapping"]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,7 @@ authorizationEncryptedResponseAlgHelp=JWA Algorithm used for key management in e
deleteConfirmGroup_other=Are you sure you want to delete these groups.
scopePermissions.users.manage-description=Policies that decide if an administrator can manage all users in the realm
defaultACRValuesHelp=Default values to be used as voluntary ACR in case that there is no explicit ACR requested by 'claims' or 'acr_values' parameter in the OIDC request.
minimumACRValueHelp=Minimum ACR to be enforced by keycloak. Overrides lower ACRs explicitly requested via 'acr_values' or 'claims', unless marked they are essential
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be "minimumACRValuesHelp"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a singular value, but I just realized in the UI part of the PR I added it as a MultiLine input. That was a copy-pasting accident from a previous version of this PR where it was multiple values. I'll adjust the input type.

membershipAttributeType=Membership attribute type
eventTypes.PUSHED_AUTHORIZATION_REQUEST.name=Pushed authorization request
included.client.audience.tooltip=The Client ID of the specified audience client will be included in audience (aud) field of the token. If there are existing audiences in the token, the specified value is just added to them. It won't override existing audiences.
Expand All @@ -1373,6 +1374,7 @@ otpPolicyDigitsHelp=How many digits should the OTP have?
clientAuthentications.client_secret_post=Client secret sent as post
prompts.select_account=Select account
defaultACRValues=Default ACR Values
minimumACRValue=Minimum ACR Value
valueError=A value must be provided.
noConsents=No consents
orderChangeSuccessUserFed=Successfully changed the priority order of user federation providers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HelpItem } from "@keycloak/keycloak-ui-shared";
import { HelpItem, TextControl } from "@keycloak/keycloak-ui-shared";
import {
ActionGroup,
Button,
Expand Down Expand Up @@ -249,6 +249,12 @@ export const AdvancedSettings = ({
stringify
/>
</FormGroup>
<TextControl
type="text"
name={convertAttributeNameToForm("attributes.minimum.acr.value")}
label={t("minimumACRValue")}
labelIcon={t("minimumACRValueHelp")}
/>
</>
)}
<ActionGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public final class Constants {
public static final String FORCE_LEVEL_OF_AUTHENTICATION = "force-level-of-authentication";
public static final String ACR_LOA_MAP = "acr.loa.map";
public static final String DEFAULT_ACR_VALUES = "default.acr.values";
public static final String MINIMUM_ACR_VALUE = "minimum.acr.value";
public static final int MINIMUM_LOA = 0;
public static final int NO_LOA = -1;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.util.DPoPUtil;
import org.keycloak.utils.StringUtil;
Expand Down Expand Up @@ -106,7 +107,7 @@ public void setRequestObjectEncryptionEnc(String algorithm) {
public String getRequestObjectRequired() {
return getAttribute(OIDCConfigAttributes.REQUEST_OBJECT_REQUIRED);
}

public void setRequestObjectRequired(String requestObjectRequired) {
setAttribute(OIDCConfigAttributes.REQUEST_OBJECT_REQUIRED, requestObjectRequired);
}
Expand Down Expand Up @@ -413,4 +414,11 @@ public void setPostLogoutRedirectUris(List<String> postLogoutRedirectUris) {
setAttributeMultivalued(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, postLogoutRedirectUris);
}

public String getMinimumAcrValue() {
return getAttribute(Constants.MINIMUM_ACR_VALUE);
}

public void setMinimumAcrValue(String minimumAcrValue) {
setAttribute(Constants.MINIMUM_ACR_VALUE, minimumAcrValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
Expand Down Expand Up @@ -317,6 +318,14 @@ private void updateAuthenticationSession() {
if (acrValues.isEmpty()) {
acrValues = AcrUtils.getAcrValues(request.getClaims(), request.getAcr(), authenticationSession.getClient());
} else {
List<String> minimizedAcrValues = AcrUtils.enforceMinimumAcr(acrValues, client);
// If enforcing a minimum here changes the list, the client has an essential claim that is too low
if (!minimizedAcrValues.equals(acrValues)) {
logger.errorf("Requested essential acr value list contains values lower than the client minimum. Please doublecheck the client configuration or correct ACR passed in the 'claims' parameter.");
event.detail(Details.REASON, "Invalid requested essential acr value");
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.CLAIMS_PARAM);
}
authenticationSession.setClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION, "true");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.jboss.logging.Logger;
import org.keycloak.authentication.authenticators.util.LoAUtil;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
Expand All @@ -42,14 +46,61 @@ public static List<String> getRequiredAcrValues(String claimsParam) {
return getAcrValues(claimsParam, null, true);
}


public static List<String> getAcrValues(String claimsParam, String acrValuesParam, ClientModel client) {
List<String> fromParams = getAcrValues(claimsParam, acrValuesParam, false);
if (!fromParams.isEmpty()) {
return fromParams;
List<String> acrValues = getAcrValues(claimsParam, acrValuesParam, false);

if (acrValues.isEmpty()) {
// Fallback to default ACR values of client (if configured)
acrValues = getDefaultAcrValues(client);
}
return enforceMinimumAcr(acrValues, client);
}

public static List<String> enforceMinimumAcr(List<String> acrValues, ClientModel client) {
String minimumAcr = getMinimumAcrValue(client);

// Fallback to default ACR values of client (if configured)
return getDefaultAcrValues(client);
// If a minimum is set, we need to validate the client didn't request a lower ACR
if (minimumAcr != null) {
List<String> acrCopy = new ArrayList<>(acrValues);
Map<String, Integer> acrMap = getAcrLoaMap(client);
Integer minimumLoa = getLoaForAcr(minimumAcr, acrMap, client);
if (minimumLoa == null) {
LOGGER.warnf("ACR '%s' can not be mapped to a LoA value.", minimumAcr);
} else {
// Remove all ACRs lower than the minimum
Iterator<String> iterator = acrCopy.iterator();
while (iterator.hasNext()) {
String acrValue = iterator.next();
Integer loa = getLoaForAcr(acrValue, acrMap, client);
if (loa == null) {
LOGGER.warnf("ACR '%s' can not be mapped to a LoA value.", acrValue);
iterator.remove();
} else if (loa < minimumLoa) {
iterator.remove();
}
}
// All ACRs lower than the minimum are gone, if we have none left, add our minimum
if (acrCopy.isEmpty()) {
acrCopy.add(minimumAcr);
}
}
return acrCopy;
}
return acrValues;
}

private static Integer getLoaForAcr(String acr, Map<String, Integer> acrMap, ClientModel client) {
Integer loa = acrMap.get(acr);
if (loa == null) {
Optional<Integer> loaFromFlows = LoAUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm())
.filter(l -> acr.equals(String.valueOf(l)))
.findFirst();
if (loaFromFlows.isPresent()) {
loa = loaFromFlows.get();
}
}
return loa;
}

private static List<String> getAcrValues(String claimsParam, String acrValuesParam, boolean essential) {
Expand Down Expand Up @@ -152,4 +203,8 @@ public static String mapLoaToAcr(int loa, Map<String, Integer> acrLoaMap, Collec
public static List<String> getDefaultAcrValues(ClientModel client) {
return OIDCAdvancedConfigWrapper.fromClientModel(client).getAttributeMultivalued(Constants.DEFAULT_ACR_VALUES);
}

public static String getMinimumAcrValue(ClientModel client) {
return OIDCAdvancedConfigWrapper.fromClientModel(client).getMinimumAcrValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;

import java.net.MalformedURLException;
import java.net.URI;
Expand Down Expand Up @@ -185,6 +186,7 @@ public ValidationResult validate(ValidationContext<ClientModel> context) {
new CibaClientValidation(context).validate();
validateJwks(context);
validateDefaultAcrValues(context);
validateMinimumAcrValue(context);

return context.toResult();
}
Expand All @@ -195,6 +197,7 @@ public ValidationResult validate(ClientValidationContext.OIDCContext context) {
validatePairwiseInOIDCClient(context);
new CibaClientValidation(context).validate();
validateDefaultAcrValues(context);
validateMinimumAcrValue(context);

return context.toResult();
}
Expand Down Expand Up @@ -379,10 +382,28 @@ private void validateDefaultAcrValues(ValidationContext<ClientModel> context) {
}
for (String configuredAcr : defaultAcrValues) {
if (acrToLoaMap.containsKey(configuredAcr)) continue;
if (!LoAUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm())
.anyMatch(level -> configuredAcr.equals(String.valueOf(level)))) {
if (LoAUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm())
.noneMatch(level -> configuredAcr.equals(String.valueOf(level)))) {
context.addError("defaultAcrValues", "Default ACR values need to contain values specified in the ACR-To-Loa mapping or number levels from set realm browser flow");
}
}
}

private void validateMinimumAcrValue(ValidationContext<ClientModel> context) {
ClientModel client = context.getObjectToValidate();
String minimumAcrValue = AcrUtils.getMinimumAcrValue(client);
if (minimumAcrValue != null) {
Map<String, Integer> acrToLoaMap = AcrUtils.getAcrLoaMap(client);
if (acrToLoaMap.isEmpty()) {
acrToLoaMap = AcrUtils.getAcrLoaMap(client.getRealm());
}

if(!acrToLoaMap.containsKey(minimumAcrValue)) {
if (LoAUtil.getLoAConfiguredInRealmBrowserFlow(client.getRealm())
.noneMatch(level -> minimumAcrValue.equals(String.valueOf(level)))) {
context.addError("minimumAcrValue", "Minimum ACR value needs to be value specified in the ACR-To-Loa mapping or number level from set realm browser flow");
}
}
}
}
}
Loading