diff --git a/core/src/main/java/org/keycloak/representations/ClaimsRepresentation.java b/core/src/main/java/org/keycloak/representations/ClaimsRepresentation.java
new file mode 100644
index 0000000000..a6f7a6a523
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/ClaimsRepresentation.java
@@ -0,0 +1,154 @@
+/*
+ * 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.representations;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Claims parameter as described in the OIDC specification https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
+ *
+ * @author Marek Posolda
+ */
+public class ClaimsRepresentation {
+
+ @JsonProperty("id_token")
+ private Map idTokenClaims;
+
+ @JsonProperty("userinfo")
+ private Map userinfoClaims;
+
+ public Map getIdTokenClaims() {
+ return idTokenClaims;
+ }
+
+ public void setIdTokenClaims(Map idTokenClaims) {
+ this.idTokenClaims = idTokenClaims;
+ }
+
+ public Map getUserinfoClaims() {
+ return userinfoClaims;
+ }
+
+ public void setUserinfoClaims(Map userinfoClaims) {
+ this.userinfoClaims = userinfoClaims;
+ }
+
+ // Helper methods
+
+ /**
+ *
+ * @param claimName
+ * @param ctx Whether we ask for claim to be presented in idToken or userInfo
+ * @return true if claim is presented in the claims parameter either as "null" claim (See OIDC specification for definition of null claim) or claim with some value
+ */
+ public boolean isPresent(String claimName, ClaimContext ctx) {
+ if (ctx == ClaimContext.ID_TOKEN) {
+ return idTokenClaims != null && idTokenClaims.containsKey(claimName);
+ } else if (ctx == ClaimContext.USERINFO){
+ return userinfoClaims != null && userinfoClaims.containsKey(claimName);
+ } else {
+ throw new IllegalArgumentException("Invalid claim context");
+ }
+ }
+
+ /**
+ *
+ * @param claimName
+ * @param ctx Whether we ask for claim to be presented in idToken or userInfo
+ * @return true if claim is presented in the claims parameter as "null" claim (See OIDC specification for definition of null claim)
+ */
+ public boolean isPresentAsNullClaim(String claimName, ClaimContext ctx) {
+ if (!isPresent(claimName, ctx)) return false;
+
+ if (ctx == ClaimContext.ID_TOKEN) {
+ return idTokenClaims.get(claimName) == null;
+ } else if (ctx == ClaimContext.USERINFO){
+ return userinfoClaims.get(claimName) == null;
+ } else {
+ throw new IllegalArgumentException("Invalid claim context");
+ }
+ }
+
+ /**
+ *
+ * @param claimName
+ * @param ctx Whether we ask for claim to be presented in idToken or userInfo
+ * @param claimType claimType class
+ * @return Claim value
+ */
+ public ClaimValue getClaimValue(String claimName, ClaimContext ctx, Class claimType) {
+ if (!isPresent(claimName, ctx)) return null;
+
+ if (ctx == ClaimContext.ID_TOKEN) {
+ return (ClaimValue) idTokenClaims.get(claimName);
+ } else if (ctx == ClaimContext.USERINFO){
+ return (ClaimValue) userinfoClaims.get(claimName);
+ } else {
+ throw new IllegalArgumentException("Invalid claim context");
+ }
+ }
+
+ public enum ClaimContext {
+ ID_TOKEN, USERINFO
+ }
+
+ /**
+ * @param Specifies the type of the claim
+ */
+ public static class ClaimValue {
+
+ private Boolean essential;
+
+ private CLAIM_TYPE value;
+
+ private List values;
+
+ public Boolean getEssential() {
+ return essential;
+ }
+
+ public boolean isEssential() {
+ return essential != null && essential;
+ }
+
+ public void setEssential(Boolean essential) {
+ this.essential = essential;
+ }
+
+ public CLAIM_TYPE getValue() {
+ return value;
+ }
+
+ public void setValue(CLAIM_TYPE value) {
+ this.value = value;
+ }
+
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+ }
+}
diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java
index b1640fcf60..acdf0c8eca 100755
--- a/core/src/test/java/org/keycloak/JsonParserTest.java
+++ b/core/src/test/java/org/keycloak/JsonParserTest.java
@@ -19,6 +19,8 @@ package org.keycloak;
import org.junit.Assert;
import org.junit.Test;
+import org.keycloak.common.util.ObjectUtil;
+import org.keycloak.representations.ClaimsRepresentation;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.adapters.config.AdapterConfig;
@@ -217,5 +219,48 @@ public class JsonParserTest {
Assert.assertNull(configRep.getConfigAsMap().get("not-existing-option"));
}
+ @Test
+ public void testReadClaimsParameter() throws Exception {
+ InputStream is = getClass().getClassLoader().getResourceAsStream("sample-claims.json");
+ ClaimsRepresentation claimsRep = JsonSerialization.readValue(is, ClaimsRepresentation.class);
+
+ Assert.assertTrue(claimsRep.isPresent("auth_time", ClaimsRepresentation.ClaimContext.ID_TOKEN));
+ Assert.assertFalse(claimsRep.isPresent("auth_time", ClaimsRepresentation.ClaimContext.USERINFO));
+
+ Assert.assertFalse(claimsRep.isPresentAsNullClaim("auth_time", ClaimsRepresentation.ClaimContext.ID_TOKEN));
+ Assert.assertTrue(claimsRep.isPresentAsNullClaim("nickname", ClaimsRepresentation.ClaimContext.USERINFO));
+ Assert.assertNull(claimsRep.getClaimValue("nickname", ClaimsRepresentation.ClaimContext.USERINFO, String.class));
+
+ ClaimsRepresentation.ClaimValue email = claimsRep.getClaimValue("email", ClaimsRepresentation.ClaimContext.USERINFO, String.class);
+ assertClaimValue(email, true, null);
+
+ ClaimsRepresentation.ClaimValue emailVerified = claimsRep.getClaimValue("email_verified", ClaimsRepresentation.ClaimContext.USERINFO, Boolean.class);
+ assertClaimValue(emailVerified, true, null);
+ Assert.assertTrue(emailVerified.isEssential());
+
+ emailVerified = claimsRep.getClaimValue("email_verified", ClaimsRepresentation.ClaimContext.ID_TOKEN, Boolean.class);
+ assertClaimValue(emailVerified, false, true);
+ Assert.assertFalse(emailVerified.isEssential());
+
+ ClaimsRepresentation.ClaimValue sub = claimsRep.getClaimValue("sub", ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
+ assertClaimValue(sub, null, "248289761001");
+ Assert.assertFalse(sub.isEssential());
+
+ ClaimsRepresentation.ClaimValue acr = claimsRep.getClaimValue("acr", ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
+ assertClaimValue(acr, null, null, "urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:gold");
+ }
+
+ private void assertClaimValue(ClaimsRepresentation.ClaimValue claimVal, Boolean expectedEssential, T expectedValue, T... expectedValues) {
+ Assert.assertTrue(ObjectUtil.isEqualOrBothNull(expectedEssential, claimVal.getEssential()));
+ Assert.assertTrue(ObjectUtil.isEqualOrBothNull(expectedValue, claimVal.getValue()));
+
+ if (expectedValues == null) {
+ Assert.assertNull(claimVal.getValues());
+ } else {
+ for (int i = 0; iMarek Posolda
+ */
+public interface AuthenticationFlowCallback extends Authenticator {
+
+ /**
+ * Triggered after the authentication flow is successfully finished. The target authentication flow is the one where this
+ * authenticator is configured. Authenticator should finish successfully in the flow (or being evaluated to true in case of Conditional Authenticator)
+ * in order to trigger this callback at the successful end of the flow
+ *
+ * @param context which encapsulate various useful data
+ */
+ void onParentFlowSuccess(AuthenticationFlowContext context);
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowCallbackFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowCallbackFactory.java
new file mode 100644
index 0000000000..ada7082cdf
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowCallbackFactory.java
@@ -0,0 +1,27 @@
+/*
+ * 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.authentication;
+
+/**
+ * Factory to create {@link AuthenticationFlowCallback} instances. Mostly used as marker interface.
+ *
+ * @author Marek Posolda
+ */
+public interface AuthenticationFlowCallbackFactory extends AuthenticatorFactory {
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
index 014512a848..348d02dcc6 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
@@ -130,4 +130,12 @@ public final class Constants {
public static final String CLIENT_PROFILES = "client-policies.profiles";
public static final String CLIENT_POLICIES = "client-policies.policies";
+
+ public static final String LEVEL_OF_AUTHENTICATION = "level-of-authentication";
+ public static final String REQUESTED_LEVEL_OF_AUTHENTICATION = "requested-level-of-authentication";
+ 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 int MINIMUM_LOA = 0;
+ public static final int MAXIMUM_LOA = Integer.MAX_VALUE;
+ public static final int NO_LOA = -1;
}
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationSelectionResolver.java b/services/src/main/java/org/keycloak/authentication/AuthenticationSelectionResolver.java
index 221d0bdec9..266eade1eb 100644
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationSelectionResolver.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationSelectionResolver.java
@@ -200,7 +200,7 @@ class AuthenticationSelectionResolver {
// For conditional execution, we must check if condition is true. Otherwise return false, which means trying next
// requiredExecution in the list
- return !flow.isConditionalSubflowDisabled(ex);
+ return !flow.isConditionalSubflowDisabled(ex, false);
}).findFirst().orElse(null);
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java
new file mode 100755
index 0000000000..e5deb35613
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java
@@ -0,0 +1,43 @@
+/*
+ * 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.authentication;
+
+import org.keycloak.models.Constants;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+public class AuthenticatorUtil {
+
+ public static boolean isLevelOfAuthenticationForced(AuthenticationSessionModel authSession) {
+ return Boolean.parseBoolean(authSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION));
+ }
+
+ public static int getRequestedLevelOfAuthentication(AuthenticationSessionModel authSession) {
+ String requiredLoa = authSession.getClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION);
+ return requiredLoa == null ? Constants.NO_LOA : Integer.parseInt(requiredLoa);
+ }
+
+ public static int getCurrentLevelOfAuthentication(AuthenticationSessionModel authSession) {
+ String authSessionLoaNote = authSession.getAuthNote(Constants.LEVEL_OF_AUTHENTICATION);
+ return authSessionLoaNote == null ? Constants.NO_LOA : Integer.parseInt(authSessionLoaNote);
+ }
+
+ public static boolean isLevelOfAuthenticationSatisfied(AuthenticationSessionModel authSession) {
+ return AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession)
+ <= AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index d8dbd81b1d..031e51afa3 100755
--- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
@@ -26,6 +26,7 @@ import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MultivaluedMap;
@@ -146,7 +147,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
checkAndValidateParentFlow(model);
return processFlow();
} else {
- processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
+ setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return flowChallenge;
}
}
@@ -169,6 +170,8 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
/**
* Called after "actionExecutionModel" execution is finished (Either successful or attempted). Find the next appropriate authentication
* flow where the authentication should continue and continue with authentication process.
+ * The method recursively continues with the parent flow
+ * until finally the top flow is processed.
*
* @param actionExecutionModel
* @return Response if some more forms should be displayed during authentication. Null otherwise.
@@ -185,8 +188,8 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
} else {
Response response = processSingleFlowExecutionModel(parentFlowExecution, false);
if (response == null) {
- processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
- return processFlow();
+ // the parent flow is now the last action that has been executed, continue with that until the top flow is reached
+ return continueAuthenticationAfterSuccessfulAction(parentFlowExecution);
} else {
return response;
}
@@ -214,10 +217,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
requiredExecutions, alternativeExecutions);
// Note: If we evaluate alternative execution, we will also doublecheck that there are not required elements in same subflow
- if ((model.isRequired() && requiredExecutions.stream().allMatch(processor::isSuccessful)) ||
+ if (((model.isRequired() || model.isConditional()) && requiredExecutions.stream().allMatch(processor::isSuccessful)) ||
(model.isAlternative() && alternativeExecutions.stream().anyMatch(processor::isSuccessful) && requiredExecutions.isEmpty())) {
logger.debugf("Flow '%s' successfully finished after children executions success", logExecutionAlias(parentFlowExecutionModel));
- processor.getAuthenticationSession().setExecutionStatus(parentFlowExecutionModel.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
+ setExecutionStatus(parentFlowExecutionModel, AuthenticationSessionModel.ExecutionStatus.SUCCESS);
// Flow is successfully finished. Recursively check whether it's parent flow is now successful as well
model = parentFlowExecutionModel;
@@ -246,7 +249,8 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
while (requiredIListIterator.hasNext()) {
AuthenticationExecutionModel required = requiredIListIterator.next();
//Conditional flows must be considered disabled (non-existent) if their condition evaluates to false.
- if (required.isConditional() && isConditionalSubflowDisabled(required)) {
+ //If the flow has been processed before it will not be removed to consider its execution status.
+ if (required.isConditional() && !isProcessed(required) && isConditionalSubflowDisabled(required, true)) {
requiredIListIterator.remove();
continue;
}
@@ -284,7 +288,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
} catch (AuthenticationFlowException afe) {
//consuming the error is not good here from an administrative point of view, but the user, since he has alternatives, should be able to go to another alternative and continue
afeList.add(afe);
- processor.getAuthenticationSession().setExecutionStatus(alternative.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
+ setExecutionStatus(alternative, AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
}
}
} else {
@@ -322,9 +326,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
/**
* Checks if the conditional subflow passed in parameter is disabled.
* @param model
+ * @param storeResult whether to store the result of the conditional evaluations
* @return
*/
- boolean isConditionalSubflowDisabled(AuthenticationExecutionModel model) {
+ boolean isConditionalSubflowDisabled(AuthenticationExecutionModel model, boolean storeResult) {
if (model == null || !model.isAuthenticatorFlow() || !model.isConditional()) {
return false;
};
@@ -335,7 +340,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
.filter(s -> s.isEnabled())
.collect(Collectors.toList());
return conditionalAuthenticatorList.isEmpty() || conditionalAuthenticatorList.stream()
- .anyMatch(m -> conditionalNotMatched(m, modelList));
+ .anyMatch(m -> conditionalNotMatched(m, modelList, storeResult));
}
private boolean isConditionalAuthenticator(AuthenticationExecutionModel model) {
@@ -350,12 +355,12 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
return factory;
}
- private boolean conditionalNotMatched(AuthenticationExecutionModel model, List executionList) {
+ private boolean conditionalNotMatched(AuthenticationExecutionModel model, List executionList, boolean storeResult) {
AuthenticatorFactory factory = getAuthenticatorFactory(model);
ConditionalAuthenticator authenticator = (ConditionalAuthenticator) createAuthenticator(factory);
AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executionList);
- boolean matchCondition;
+ boolean matchCondition;
// Retrieve previous evaluation result if any, else evaluate and store result for future re-evaluation
if (processor.isEvaluatedTrue(model)) {
@@ -364,8 +369,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
matchCondition = false;
} else {
matchCondition = authenticator.matchCondition(context);
- processor.getAuthenticationSession().setExecutionStatus(model.getId(),
- matchCondition ? AuthenticationSessionModel.ExecutionStatus.EVALUATED_TRUE : AuthenticationSessionModel.ExecutionStatus.EVALUATED_FALSE);
+ if (storeResult) {
+ setExecutionStatus(model,
+ matchCondition ? AuthenticationSessionModel.ExecutionStatus.EVALUATED_TRUE : AuthenticationSessionModel.ExecutionStatus.EVALUATED_FALSE);
+ }
}
return !matchCondition;
@@ -390,14 +397,14 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (flowChallenge == null) {
if (authenticationFlow.isSuccessful()) {
logger.debugf("Flow '%s' successfully finished", logExecutionAlias(model));
- processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
+ setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.SUCCESS);
} else {
logger.debugf("Flow '%s' failed", logExecutionAlias(model));
- processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.FAILED);
+ setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.FAILED);
}
return null;
} else {
- processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
+ setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return flowChallenge;
}
}
@@ -434,7 +441,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (factory.isUserSetupAllowed() && model.isRequired() && authenticator.areRequiredActionsEnabled(processor.getSession(), processor.getRealm())) {
//This means that having even though the user didn't validate the
logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId());
- processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED);
+ setExecutionStatus(model, AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED);
authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser());
return null;
} else {
@@ -485,12 +492,12 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
switch (status) {
case SUCCESS:
logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
- processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
+ setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.SUCCESS);
return null;
case FAILED:
logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator());
processor.logFailure();
- processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.FAILED);
+ setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.FAILED);
if (result.getChallenge() != null) {
return sendChallenge(result, execution);
}
@@ -501,16 +508,16 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage());
case FORCE_CHALLENGE:
case CHALLENGE:
- processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
+ setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
case FAILURE_CHALLENGE:
logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
processor.logFailure();
- processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
+ setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
case ATTEMPTED:
logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
- processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
+ setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
return null;
case FLOW_RESET:
processor.resetFlow();
@@ -536,4 +543,28 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
public List getFlowExceptions(){
return afeList;
}
+
+ private void setExecutionStatus(AuthenticationExecutionModel authExecutionModel, CommonClientSessionModel.ExecutionStatus status) {
+ this.processor.getAuthenticationSession().setExecutionStatus(authExecutionModel.getId(), status);
+
+ if (authExecutionModel.isAuthenticatorFlow() && status == CommonClientSessionModel.ExecutionStatus.SUCCESS) {
+ // Trigger callbacks after flow was successfully finished
+ processor.getRealm().getAuthenticationExecutionsStream(authExecutionModel.getFlowId()).forEach(this::checkAuthCallback);
+ }
+ }
+
+ private void checkAuthCallback(AuthenticationExecutionModel execution) {
+ // We will trigger the callback just if particular authenticator, which corresponds to this callback, was finished with SUCCESS or condition was evaluated to true
+ CommonClientSessionModel.ExecutionStatus executionStatus = processor.getAuthenticationSession().getExecutionStatus().get(execution.getId());
+ if (executionStatus == CommonClientSessionModel.ExecutionStatus.SUCCESS || executionStatus == CommonClientSessionModel.ExecutionStatus.EVALUATED_TRUE) {
+ if (!execution.isAuthenticatorFlow()) {
+ AuthenticatorFactory authFactory = getAuthenticatorFactory(execution);
+ if (authFactory instanceof AuthenticationFlowCallbackFactory) {
+ AuthenticationFlowCallback authCallback = (AuthenticationFlowCallback) createAuthenticator(authFactory);
+ logger.tracef("Will trigger callback '%s' after successful finish of the flow '%s'", authFactory.getId(), execution.getParentFlow());
+ authCallback.onParentFlowSuccess(processor.createAuthenticatorContext(execution, authCallback, null)); // no need to have executions filled
+ }
+ }
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java
index 04597ae4d3..76a8fa61c7 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java
@@ -19,6 +19,8 @@ package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorUtil;
+import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -44,16 +46,24 @@ public class CookieAuthenticator implements Authenticator {
if (authResult == null) {
context.attempted();
} else {
- AuthenticationSessionModel clientSession = context.getAuthenticationSession();
- LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, clientSession.getProtocol());
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+ LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol());
+ authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, authResult.getSession().getNote(Constants.LEVEL_OF_AUTHENTICATION));
+ context.setUser(authResult.getUser());
// Cookie re-authentication is skipped if re-authentication is required
- if (protocol.requireReauthentication(authResult.getSession(), clientSession)) {
+ if (protocol.requireReauthentication(authResult.getSession(), authSession)) {
+ // Full re-authentication, so we start with no loa
+ authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA));
+ context.attempted();
+ } else if (!AuthenticatorUtil.isLevelOfAuthenticationSatisfied(authSession)) {
+ // Step-up authentication, we keep the loa from the existing user session.
+ // The cookie alone is not enough and other authentications must follow.
context.attempted();
} else {
- context.getAuthenticationSession().setAuthNote(AuthenticationManager.SSO_AUTH, "true");
-
- context.setUser(authResult.getUser());
+ // Cookie only authentication, no loa is returned
+ authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA));
+ authSession.setAuthNote(AuthenticationManager.SSO_AUTH, "true");
context.attachUserSession(authResult.getSession());
context.success();
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticator.java
new file mode 100644
index 0000000000..16691f838d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticator.java
@@ -0,0 +1,92 @@
+/*
+ * 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.authentication.authenticators.conditional;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authentication.AuthenticationFlowCallback;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticatorUtil;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+public class ConditionalLoaAuthenticator implements ConditionalAuthenticator, AuthenticationFlowCallback {
+
+ public static final String LEVEL = "loa-condition-level";
+ public static final String STORE_IN_USER_SESSION = "loa-store-in-user-session";
+
+ private static final Logger logger = Logger.getLogger(ConditionalLoaAuthenticator.class);
+
+ @Override
+ public boolean matchCondition(AuthenticationFlowContext context) {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+ int currentLoa = AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession);
+ int requestedLoa = AuthenticatorUtil.getRequestedLevelOfAuthentication(authSession);
+ Integer configuredLoa = getConfiguredLoa(context);
+ return (currentLoa < Constants.MINIMUM_LOA && requestedLoa < Constants.MINIMUM_LOA)
+ || ((configuredLoa == null || currentLoa < configuredLoa) && currentLoa < requestedLoa);
+ }
+
+ @Override
+ public void onParentFlowSuccess(AuthenticationFlowContext context) {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+ Integer newLoa = getConfiguredLoa(context);
+ if (newLoa == null) {
+ return;
+ }
+ logger.tracef("Updating LoA to '%d' when authenticating session '%s'", newLoa, authSession.getParentSession().getId());
+ authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(newLoa));
+ if (isStoreInUserSession(context)) {
+ authSession.setUserSessionNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(newLoa));
+ }
+ }
+
+ private Integer getConfiguredLoa(AuthenticationFlowContext context) {
+ try {
+ return Integer.parseInt(context.getAuthenticatorConfig().getConfig().get(LEVEL));
+ } catch (NullPointerException | NumberFormatException e) {
+ logger.errorv("Invalid configuration: {0}", LEVEL);
+ return null;
+ }
+ }
+
+ private boolean isStoreInUserSession(AuthenticationFlowContext context) {
+ try {
+ return Boolean.parseBoolean(context.getAuthenticatorConfig().getConfig().get(STORE_IN_USER_SESSION));
+ } catch (NullPointerException | NumberFormatException e) {
+ logger.errorv("Invalid configuration: {0}", STORE_IN_USER_SESSION);
+ return false;
+ }
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) { }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { }
+
+ @Override
+ public void close() { }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticatorFactory.java
new file mode 100644
index 0000000000..1f1a7ab0f4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalLoaAuthenticatorFactory.java
@@ -0,0 +1,106 @@
+/*
+ * 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.authentication.authenticators.conditional;
+
+import java.util.List;
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticationFlowCallbackFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+
+public class ConditionalLoaAuthenticatorFactory implements ConditionalAuthenticatorFactory, AuthenticationFlowCallbackFactory {
+
+ public static final String PROVIDER_ID = "conditional-level-of-authentication";
+ private static final ConditionalLoaAuthenticator SINGLETON = new ConditionalLoaAuthenticator();
+ private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = new AuthenticationExecutionModel.Requirement[]{
+ AuthenticationExecutionModel.Requirement.REQUIRED,
+ AuthenticationExecutionModel.Requirement.DISABLED
+ };
+
+ private static final List CONFIG = ProviderConfigurationBuilder.create()
+ .property()
+ .name(ConditionalLoaAuthenticator.LEVEL)
+ .label(ConditionalLoaAuthenticator.LEVEL)
+ .helpText(ConditionalLoaAuthenticator.LEVEL + ".tooltip")
+ .type(ProviderConfigProperty.STRING_TYPE)
+ .add()
+ .property()
+ .name(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION)
+ .label(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION)
+ .helpText(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION + ".tooltip")
+ .type(ProviderConfigProperty.BOOLEAN_TYPE)
+ .defaultValue("true")
+ .add()
+ .build();
+
+ @Override
+ public void init(Config.Scope config) { }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) { }
+
+ @Override
+ public void close() { }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Condition - Level of Authentication";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return "condition";
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return true;
+ }
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Flow is executed only if the configured LOA or a higher one has been requested but not yet satisfied. After the flow is successfully finished, the LOA in the session will be updated to value prescribed by this condition.";
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return CONFIG;
+ }
+
+ @Override
+ public ConditionalAuthenticator getSingleton() {
+ return SINGLETON;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index e1bca46d7f..00442efb84 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -24,6 +24,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenCategory;
import org.keycloak.TokenVerifier;
+import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.cluster.ClusterProvider;
@@ -43,6 +44,7 @@ import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
+import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -57,6 +59,7 @@ import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenResponseMapper;
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;
+import org.keycloak.protocol.oidc.utils.AcrUtils;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
@@ -555,6 +558,7 @@ public class TokenManager {
userSession.setNote(entry.getKey(), entry.getValue());
}
+ clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(AuthenticatorUtil.getCurrentLevelOfAuthentication(authSession)));
clientSession.setTimestamp(Time.currentTime());
// Remove authentication session now
@@ -832,10 +836,7 @@ public class TokenManager {
token.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class));
token.setScope(clientSessionCtx.getScopeString());
- // Best effort for "acr" value. Use 0 if clientSession was authenticated through cookie ( SSO )
- // TODO: Add better acr support. See KEYCLOAK-3314
- String acr = (AuthenticationManager.isSSOAuthentication(clientSession)) ? "0" : "1";
- token.setAcr(acr);
+ token.setAcr(getAcr(clientSession));
String authTime = session.getNote(AuthenticationManager.AUTH_TIME);
if (authTime != null) {
@@ -852,6 +853,29 @@ public class TokenManager {
return token;
}
+ private String getAcr(AuthenticatedClientSessionModel clientSession) {
+ int loa = Integer.parseInt(clientSession.getNote(Constants.LEVEL_OF_AUTHENTICATION));
+ if (loa < Constants.MINIMUM_LOA) {
+ loa = AuthenticationManager.isSSOAuthentication(clientSession) ? 0 : 1;
+ }
+
+ Map acrLoaMap = AcrUtils.getAcrLoaMap(clientSession.getClient());
+ String acr = AcrUtils.mapLoaToAcr(loa, acrLoaMap, AcrUtils.getRequiredAcrValues(
+ clientSession.getNote(OIDCLoginProtocol.CLAIMS_PARAM)));
+ if (acr == null) {
+ acr = AcrUtils.mapLoaToAcr(loa, acrLoaMap, AcrUtils.getAcrValues(
+ clientSession.getNote(OIDCLoginProtocol.CLAIMS_PARAM),
+ clientSession.getNote(OIDCLoginProtocol.ACR_PARAM)));
+ if (acr == null) {
+ acr = AcrUtils.mapLoaToAcr(loa, acrLoaMap, acrLoaMap.keySet());
+ if (acr == null) {
+ acr = String.valueOf(loa);
+ }
+ }
+ }
+ return acr;
+ }
+
private int getTokenExpiration(RealmModel realm, ClientModel client, UserSessionModel userSession,
AuthenticatedClientSessionModel clientSession, boolean offlineTokenRequested) {
boolean implicitFlow = false;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 7b46fadb2c..a2f9dd2fac 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -35,6 +35,7 @@ import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
+import org.keycloak.protocol.oidc.utils.AcrUtils;
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
@@ -57,6 +58,11 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.List;
+import java.util.Map;
+
/**
* @author Stian Thorgersen
*/
@@ -286,6 +292,26 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
if (request.getCodeChallengeMethod() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
+ Map acrLoaMap = AcrUtils.getAcrLoaMap(authenticationSession.getClient());
+ List acrValues = AcrUtils.getRequiredAcrValues(request.getClaims());
+
+ if (acrValues.isEmpty()) {
+ acrValues = AcrUtils.getAcrValues(request.getClaims(), request.getAcr());
+ } else {
+ authenticationSession.setClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION, "true");
+ }
+
+ acrValues.stream().mapToInt(acr -> {
+ try {
+ Integer loa = acrLoaMap.get(acr);
+ return loa == null ? Integer.parseInt(acr) : loa;
+ } catch (NumberFormatException e) {
+ // this is an unknown acr, we assume in case of an essential claim a very high LoA, and a minimum LoA if not essential
+ return Boolean.parseBoolean(authenticationSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION))
+ ? Constants.MAXIMUM_LOA : Constants.MINIMUM_LOA;
+ }
+ }).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));
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AcrUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AcrUtils.java
new file mode 100644
index 0000000000..f2927851f7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AcrUtils.java
@@ -0,0 +1,105 @@
+/*
+ * 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.protocol.oidc.utils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.jboss.logging.Logger;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.representations.ClaimsRepresentation;
+import org.keycloak.representations.IDToken;
+import org.keycloak.util.JsonSerialization;
+
+public class AcrUtils {
+
+ private static final Logger LOGGER = Logger.getLogger(AcrUtils.class);
+
+ public static List getRequiredAcrValues(String claimsParam) {
+ return getAcrValues(claimsParam, null, false);
+ }
+
+ public static List getAcrValues(String claimsParam, String acrValuesParam) {
+ return getAcrValues(claimsParam, acrValuesParam, true);
+ }
+
+ private static List getAcrValues(String claimsParam, String acrValuesParam, boolean notEssential) {
+ List acrValues = new ArrayList<>();
+ if (acrValuesParam != null && notEssential) {
+ acrValues.addAll(Arrays.asList(acrValuesParam.split(" ")));
+ }
+ if (claimsParam != null) {
+ try {
+ ClaimsRepresentation claims = JsonSerialization.readValue(claimsParam, ClaimsRepresentation.class);
+ ClaimsRepresentation.ClaimValue acrClaim = claims.getClaimValue(IDToken.ACR, ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
+ if (acrClaim != null) {
+ if (notEssential || acrClaim.isEssential()) {
+ if (acrClaim.getValues() != null) {
+ acrValues.addAll(acrClaim.getValues());
+ }
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.warn("Invalid claims parameter", e);
+ }
+ }
+ return acrValues;
+ }
+
+ public static Map getAcrLoaMap(ClientModel client) {
+ String map = client.getAttribute(Constants.ACR_LOA_MAP);
+ if (map == null || map.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ try {
+ return JsonSerialization.readValue(map, new TypeReference