From a06f50b4ef8d6674341903b68daa9dea41861ddc Mon Sep 17 00:00:00 2001
From: Sven-Torben Janus
Date: Tue, 7 Feb 2023 18:30:42 +0100
Subject: [PATCH 1/2] Support to enforce LoA in authentication flow (Step-up)
Closes #16884
---
.../browser/ForceLoAAuthenticator.java | 87 +++++++++++++++++++
.../browser/ForceLoAAuthenticatorConfig.java | 33 +++++++
...ForceLoAAuthenticatorConfigProperties.java | 26 ++++++
.../browser/ForceLoAAuthenticatorFactory.java | 79 +++++++++++++++++
.../ConditionalClientIdAuthenticator.java | 41 +++++++++
...nditionalClientIdAuthenticatorFactory.java | 82 +++++++++++++++++
.../ConditionalClientIdConfig.java | 47 ++++++++++
.../ConditionalClientIdConfigProperties.java | 38 ++++++++
...ycloak.authentication.AuthenticatorFactory | 2 +
.../admin/authentication/ProvidersTest.java | 6 ++
10 files changed, 441 insertions(+)
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticator.java
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticatorConfig.java
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticatorConfigProperties.java
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticatorFactory.java
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalClientIdAuthenticator.java
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalClientIdAuthenticatorFactory.java
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalClientIdConfig.java
create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalClientIdConfigProperties.java
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticator.java
new file mode 100644
index 000000000000..9df89475a298
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticator.java
@@ -0,0 +1,87 @@
+package org.keycloak.authentication.authenticators.browser;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.authenticators.util.AcrStore;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.utils.AcrUtils;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.models.Constants.NO_LOA;
+
+public class ForceLoAAuthenticator implements Authenticator {
+
+ @Override
+ public void authenticate(AuthenticationFlowContext authenticationFlowContext) {
+ int configuredMinLoa = new ForceLoAAuthenticatorConfig(
+ authenticationFlowContext.getAuthenticatorConfig()).levelOfAuthentication();
+
+ ClientModel client = authenticationFlowContext.getAuthenticationSession().getClient();
+
+ int maxDefaultLoa = getMaxDefaultLoa(client);
+ int enforcedLoA = Math.max(configuredMinLoa, maxDefaultLoa);
+
+ AuthenticationSessionModel authenticationSession = authenticationFlowContext.getAuthenticationSession();
+ AcrStore acrStore = new AcrStore(authenticationSession);
+ int requestedLevelOfAuthentication = acrStore.getRequestedLevelOfAuthentication();
+ if (requestedLevelOfAuthentication < enforcedLoA) {
+ authenticationSession.setClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION,
+ String.valueOf(enforcedLoA));
+ authenticationSession.setClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION, Boolean.TRUE.toString());
+ }
+ authenticationFlowContext.success();
+ }
+
+ private int getMaxDefaultLoa(ClientModel client) {
+ int defaultLoa = NO_LOA;
+ List defaultAcrValues = AcrUtils.getDefaultAcrValues(client);
+ Map acrToLoaMap = AcrUtils.getAcrLoaMap(client);
+ if (acrToLoaMap.isEmpty()) {
+ acrToLoaMap = AcrUtils.getAcrLoaMap(client.getRealm());
+ }
+ for (String configuredAcr : defaultAcrValues) {
+ int loa;
+ if (acrToLoaMap.containsKey(configuredAcr)) {
+ loa = acrToLoaMap.get(configuredAcr);
+ } else {
+ try {
+ loa = Integer.parseInt(configuredAcr);
+ } catch(NumberFormatException ex) {
+ loa = NO_LOA;
+ }
+ }
+ defaultLoa = Math.max(defaultLoa, loa);
+ }
+ return defaultLoa;
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext authenticationFlowContext) {
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticatorConfig.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticatorConfig.java
new file mode 100644
index 000000000000..4f340615a195
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ForceLoAAuthenticatorConfig.java
@@ -0,0 +1,33 @@
+package org.keycloak.authentication.authenticators.browser;
+
+import org.keycloak.models.AuthenticatorConfigModel;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static org.keycloak.models.Constants.NO_LOA;
+
+final class ForceLoAAuthenticatorConfig {
+
+ static final String MIN_LOA = "minLoA";
+
+ private final AuthenticatorConfigModel authenticatorConfigModel;
+
+ ForceLoAAuthenticatorConfig(AuthenticatorConfigModel configModel) {
+ this.authenticatorConfigModel = configModel;
+ }
+
+ int levelOfAuthentication() {
+ return getConfigMap()
+ .map(config -> config.getOrDefault(MIN_LOA, String.valueOf(NO_LOA)))
+ .map(str -> Optional.ofNullable(emptyToNull(str)).orElse(String.valueOf(NO_LOA)))
+ .map(Integer::parseInt)
+ .orElse(NO_LOA);
+ }
+
+ private Optional