Support for acr_values_supported in OIDC well-known endpoint (#10265)
* Support for acr_values_supported in OIDC well-known endpoint closes #10159
This commit is contained in:
parent
f2ed799b8b
commit
caf37b1f70
10 changed files with 340 additions and 36 deletions
|
@ -64,6 +64,9 @@ public class OIDCConfigurationRepresentation {
|
||||||
@JsonProperty("grant_types_supported")
|
@JsonProperty("grant_types_supported")
|
||||||
private List<String> grantTypesSupported;
|
private List<String> grantTypesSupported;
|
||||||
|
|
||||||
|
@JsonProperty("acr_values_supported")
|
||||||
|
private List<String> acrValuesSupported;
|
||||||
|
|
||||||
@JsonProperty("response_types_supported")
|
@JsonProperty("response_types_supported")
|
||||||
private List<String> responseTypesSupported;
|
private List<String> responseTypesSupported;
|
||||||
|
|
||||||
|
@ -258,6 +261,14 @@ public class OIDCConfigurationRepresentation {
|
||||||
this.grantTypesSupported = grantTypesSupported;
|
this.grantTypesSupported = grantTypesSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getAcrValuesSupported() {
|
||||||
|
return acrValuesSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcrValuesSupported(List<String> acrValuesSupported) {
|
||||||
|
this.acrValuesSupported = acrValuesSupported;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getResponseTypesSupported() {
|
public List<String> getResponseTypesSupported() {
|
||||||
return responseTypesSupported;
|
return responseTypesSupported;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,18 +18,30 @@
|
||||||
package org.keycloak.authentication;
|
package org.keycloak.authentication;
|
||||||
|
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
|
||||||
|
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.AuthenticatorConfigModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.utils.StringUtil;
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.keycloak.services.managers.AuthenticationManager.SSO_AUTH;
|
import static org.keycloak.services.managers.AuthenticationManager.SSO_AUTH;
|
||||||
|
|
||||||
public class AuthenticatorUtil {
|
public class AuthenticatorUtil {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(AuthenticatorUtil.class);
|
||||||
|
|
||||||
// It is used for identification of note included in authentication session for storing callback provider factories
|
// It is used for identification of note included in authentication session for storing callback provider factories
|
||||||
public static String CALLBACKS_FACTORY_IDS_NOTE = "callbacksFactoryProviderIds";
|
public static String CALLBACKS_FACTORY_IDS_NOTE = "callbacksFactoryProviderIds";
|
||||||
|
|
||||||
|
@ -103,4 +115,54 @@ public class AuthenticatorUtil {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param realm
|
||||||
|
* @param flowId
|
||||||
|
* @param providerId
|
||||||
|
* @return all executions of given "provider_id" type. This is deep (recursive) obtain of executions of the particular flow
|
||||||
|
*/
|
||||||
|
public static List<AuthenticationExecutionModel> getExecutionsByType(RealmModel realm, String flowId, String providerId) {
|
||||||
|
List<AuthenticationExecutionModel> executions = new LinkedList<>();
|
||||||
|
realm.getAuthenticationExecutionsStream(flowId).forEach(authExecution -> {
|
||||||
|
if (providerId.equals(authExecution.getAuthenticator())) {
|
||||||
|
executions.add(authExecution);
|
||||||
|
} else if (authExecution.isAuthenticatorFlow() && authExecution.getFlowId() != null) {
|
||||||
|
executions.addAll(getExecutionsByType(realm, authExecution.getFlowId(), providerId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return executions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param realm
|
||||||
|
* @return All LoA numbers configured in the conditions in the realm browser flow
|
||||||
|
*/
|
||||||
|
public static Stream<Integer> getLoAConfiguredInRealmBrowserFlow(RealmModel realm) {
|
||||||
|
List<AuthenticationExecutionModel> loaConditions = getExecutionsByType(realm, realm.getBrowserFlow().getId(), ConditionalLoaAuthenticatorFactory.PROVIDER_ID);
|
||||||
|
if (loaConditions.isEmpty()) {
|
||||||
|
// Default values used when step-up conditions not used in the browser authentication flow.
|
||||||
|
// This is used for backwards compatibility and in case when step-up is not configured in the authentication flow (returning 1 in case of "normal" authentication, 0 for SSO authentication)
|
||||||
|
return Stream.of(Constants.MINIMUM_LOA, 1);
|
||||||
|
} else {
|
||||||
|
Stream<Integer> configuredLoas = loaConditions.stream()
|
||||||
|
.map(authExecution -> realm.getAuthenticatorConfigById(authExecution.getAuthenticatorConfig()))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(authConfig -> {
|
||||||
|
String levelAsStr = authConfig.getConfig().get(ConditionalLoaAuthenticator.LEVEL);
|
||||||
|
try {
|
||||||
|
// Check it can be cast to number
|
||||||
|
return Integer.parseInt(levelAsStr);
|
||||||
|
} catch (NullPointerException | NumberFormatException e) {
|
||||||
|
logger.warnf("Invalid level '%s' configured for the configuration of LoA condition with alias '%s'. Level should be number.", levelAsStr, authConfig.getAlias());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Objects::nonNull);
|
||||||
|
|
||||||
|
// Add 0 as a level used for SSO cookie
|
||||||
|
return Stream.concat(Stream.of(Constants.MINIMUM_LOA), configuredLoas);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
import com.google.common.collect.Streams;
|
import com.google.common.collect.Streams;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.authentication.AuthenticatorUtil;
|
||||||
import org.keycloak.authentication.ClientAuthenticator;
|
import org.keycloak.authentication.ClientAuthenticator;
|
||||||
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||||
import org.keycloak.crypto.CekManagementProvider;
|
import org.keycloak.crypto.CekManagementProvider;
|
||||||
|
@ -28,6 +29,7 @@ import org.keycloak.crypto.SignatureProvider;
|
||||||
import org.keycloak.jose.jws.Algorithm;
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.models.CibaConfig;
|
import org.keycloak.models.CibaConfig;
|
||||||
import org.keycloak.models.ClientScopeModel;
|
import org.keycloak.models.ClientScopeModel;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
||||||
|
@ -37,6 +39,7 @@ import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
|
||||||
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
|
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
|
||||||
import org.keycloak.protocol.oidc.representations.MTLSEndpointAliases;
|
import org.keycloak.protocol.oidc.representations.MTLSEndpointAliases;
|
||||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||||
|
import org.keycloak.protocol.oidc.utils.AcrUtils;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
@ -54,6 +57,7 @@ import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -145,6 +149,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED);
|
config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED);
|
||||||
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
|
config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED);
|
||||||
config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_SUPPORTED);
|
config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_SUPPORTED);
|
||||||
|
config.setAcrValuesSupported(getAcrValuesSupported(realm));
|
||||||
|
|
||||||
config.setTokenEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
|
config.setTokenEndpointAuthMethodsSupported(getClientAuthMethodsSupported());
|
||||||
config.setTokenEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false));
|
config.setTokenEndpointAuthSigningAlgValuesSupported(getSupportedClientSigningAlgorithms(false));
|
||||||
|
@ -253,6 +258,18 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
return getSupportedAlgorithms(ContentEncryptionProvider.class, false);
|
return getSupportedAlgorithms(ContentEncryptionProvider.class, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> getAcrValuesSupported(RealmModel realm) {
|
||||||
|
// Values explicitly set on the realm mapping
|
||||||
|
Map<String, Integer> realmAcrLoaMap = AcrUtils.getAcrLoaMap(realm);
|
||||||
|
List<String> result = new ArrayList<>(realmAcrLoaMap.keySet());
|
||||||
|
|
||||||
|
// Add LoA levels configured in authentication flow in addition to the realm values
|
||||||
|
result.addAll(AuthenticatorUtil.getLoAConfiguredInRealmBrowserFlow(realm)
|
||||||
|
.map(String::valueOf)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> getSupportedEncryptionAlgorithms() {
|
private List<String> getSupportedEncryptionAlgorithms() {
|
||||||
return getSupportedAlgorithms(CekManagementProvider.class, false);
|
return getSupportedAlgorithms(CekManagementProvider.class, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import java.util.Map;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.representations.ClaimsRepresentation;
|
import org.keycloak.representations.ClaimsRepresentation;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
@ -71,7 +72,22 @@ public class AcrUtils {
|
||||||
return acrValues;
|
return acrValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param client
|
||||||
|
* @return map corresponding to "acr-to-loa" client attribute. It will fallback to realm in case "acr-to-loa" mapping not configured on client
|
||||||
|
*/
|
||||||
public static Map<String, Integer> getAcrLoaMap(ClientModel client) {
|
public static Map<String, Integer> getAcrLoaMap(ClientModel client) {
|
||||||
|
Map<String, Integer> result = getAcrLoaMapForClientOnly(client);
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
// Fallback to realm
|
||||||
|
return getAcrLoaMap(client.getRealm());
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Map<String, Integer> getAcrLoaMapForClientOnly(ClientModel client) {
|
||||||
String map = client.getAttribute(Constants.ACR_LOA_MAP);
|
String map = client.getAttribute(Constants.ACR_LOA_MAP);
|
||||||
if (map == null || map.isEmpty()) {
|
if (map == null || map.isEmpty()) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
|
@ -79,7 +95,24 @@ public class AcrUtils {
|
||||||
try {
|
try {
|
||||||
return JsonSerialization.readValue(map, new TypeReference<Map<String, Integer>>() {});
|
return JsonSerialization.readValue(map, new TypeReference<Map<String, Integer>>() {});
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.warn("Invalid client configuration (ACR-LOA map)");
|
LOGGER.warnf("Invalid client configuration (ACR-LOA map) for client '%s'", client.getClientId());
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param realm
|
||||||
|
* @return map corresponding to "acr-to-loa" realm attribute.
|
||||||
|
*/
|
||||||
|
public static Map<String, Integer> getAcrLoaMap(RealmModel realm) {
|
||||||
|
String map = realm.getAttribute(Constants.ACR_LOA_MAP);
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JsonSerialization.readValue(map, new TypeReference<Map<String, Integer>>() {});
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Invalid realm configuration (ACR-LOA map)");
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||||
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
|
import org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticator;
|
||||||
|
@ -38,12 +39,15 @@ import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.representations.ClaimsRepresentation;
|
import org.keycloak.representations.ClaimsRepresentation;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
|
import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
|
||||||
|
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||||
import org.keycloak.testsuite.pages.ErrorPage;
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||||
import org.keycloak.testsuite.pages.PasswordPage;
|
import org.keycloak.testsuite.pages.PasswordPage;
|
||||||
|
@ -81,53 +85,61 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
try {
|
try {
|
||||||
Map<String, Integer> acrLoaMap = new HashMap<>();
|
findTestApp(testRealm).setAttributes(Collections.singletonMap(Constants.ACR_LOA_MAP, getAcrToLoaMappingForClient()));
|
||||||
acrLoaMap.put("copper", 0);
|
|
||||||
acrLoaMap.put("silver", 1);
|
|
||||||
acrLoaMap.put("gold", 2);
|
|
||||||
findTestApp(testRealm).setAttributes(Collections.singletonMap(Constants.ACR_LOA_MAP, JsonSerialization.writeValueAsString(acrLoaMap)));
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getAcrToLoaMappingForClient() throws IOException {
|
||||||
|
Map<String, Integer> acrLoaMap = new HashMap<>();
|
||||||
|
acrLoaMap.put("copper", 0);
|
||||||
|
acrLoaMap.put("silver", 1);
|
||||||
|
acrLoaMap.put("gold", 2);
|
||||||
|
return JsonSerialization.writeValueAsString(acrLoaMap);
|
||||||
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setupFlow() {
|
public void setupFlow() {
|
||||||
|
configureStepUpFlow(testingClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void configureStepUpFlow(KeycloakTestingClient testingClient) {
|
||||||
final String newFlowAlias = "browser - Level of Authentication FLow";
|
final String newFlowAlias = "browser - Level of Authentication FLow";
|
||||||
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
testingClient.server(TEST_REALM_NAME).run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||||
testingClient.server(TEST_REALM_NAME)
|
testingClient.server(TEST_REALM_NAME)
|
||||||
.run(session -> FlowUtil.inCurrentRealm(session).selectFlow(newFlowAlias).inForms(forms -> forms.clear()
|
.run(session -> FlowUtil.inCurrentRealm(session).selectFlow(newFlowAlias).inForms(forms -> forms.clear()
|
||||||
// level 1 authentication
|
// level 1 authentication
|
||||||
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
|
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
|
||||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
||||||
config -> {
|
config -> {
|
||||||
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1");
|
config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "1");
|
||||||
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "true");
|
config.getConfig().put(ConditionalLoaAuthenticator.STORE_IN_USER_SESSION, "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
// username input for level 1
|
// username input for level 1
|
||||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID);
|
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID);
|
||||||
})
|
})
|
||||||
|
|
||||||
// level 2 authentication
|
// level 2 authentication
|
||||||
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
|
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
|
||||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
||||||
config -> config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "2"));
|
config -> config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "2"));
|
||||||
|
|
||||||
// password required for level 2
|
// password required for level 2
|
||||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PasswordFormFactory.PROVIDER_ID);
|
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PasswordFormFactory.PROVIDER_ID);
|
||||||
})
|
})
|
||||||
|
|
||||||
// level 3 authentication
|
// level 3 authentication
|
||||||
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
|
.addSubFlowExecution(Requirement.CONDITIONAL, subFlow -> {
|
||||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, ConditionalLoaAuthenticatorFactory.PROVIDER_ID,
|
||||||
config -> config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "3"));
|
config -> config.getConfig().put(ConditionalLoaAuthenticator.LEVEL, "3"));
|
||||||
|
|
||||||
// simply push button for level 3
|
// simply push button for level 3
|
||||||
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PushButtonAuthenticatorFactory.PROVIDER_ID);
|
subFlow.addAuthenticatorExecution(Requirement.REQUIRED, PushButtonAuthenticatorFactory.PROVIDER_ID);
|
||||||
})
|
})
|
||||||
|
|
||||||
).defineAsBrowserFlow());
|
).defineAsBrowserFlow());
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -286,6 +298,45 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertLoggedInWithAcr("gold");
|
assertLoggedInWithAcr("gold");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRealmAcrLoaMapping() throws IOException {
|
||||||
|
// Setup realm acr-to-loa mapping
|
||||||
|
RealmRepresentation realmRep = testRealm().toRepresentation();
|
||||||
|
Map<String, Integer> acrLoaMap = new HashMap<>();
|
||||||
|
acrLoaMap.put("realm:copper", 0);
|
||||||
|
acrLoaMap.put("realm:silver", 1);
|
||||||
|
acrLoaMap.put("realm:gold", 2);
|
||||||
|
realmRep.getAttributes().put(Constants.ACR_LOA_MAP, JsonSerialization.writeValueAsString(acrLoaMap));
|
||||||
|
testRealm().update(realmRep);
|
||||||
|
|
||||||
|
// Remove acr-to-loa mapping from the client. It should use realm acr-to-loa mapping
|
||||||
|
ClientResource testClient = ApiUtil.findClientByClientId(testRealm(), "test-app");
|
||||||
|
ClientRepresentation testClientRep = testClient.toRepresentation();
|
||||||
|
testClientRep.getAttributes().put(Constants.ACR_LOA_MAP, "{}");
|
||||||
|
testClient.update(testClientRep);
|
||||||
|
|
||||||
|
openLoginFormWithAcrClaim(true, "realm:gold");
|
||||||
|
authenticateWithUsername();
|
||||||
|
authenticateWithPassword();
|
||||||
|
assertLoggedInWithAcr("realm:gold");
|
||||||
|
|
||||||
|
// Add "acr-to-loa" back to the client. Client mapping will be used instead of realm mapping
|
||||||
|
testClientRep.getAttributes().put(Constants.ACR_LOA_MAP, getAcrToLoaMappingForClient());
|
||||||
|
testClient.update(testClientRep);
|
||||||
|
|
||||||
|
openLoginFormWithAcrClaim(true, "realm:gold");
|
||||||
|
assertErrorPage("Invalid parameter: claims");
|
||||||
|
|
||||||
|
openLoginFormWithAcrClaim(true, "gold");
|
||||||
|
authenticateWithPassword();
|
||||||
|
assertLoggedInWithAcr("gold");
|
||||||
|
|
||||||
|
// Rollback
|
||||||
|
realmRep.getAttributes().remove(Constants.ACR_LOA_MAP);
|
||||||
|
testRealm().update(realmRep);
|
||||||
|
}
|
||||||
|
|
||||||
public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) {
|
public void openLoginFormWithAcrClaim(boolean essential, String... acrValues) {
|
||||||
openLoginFormWithAcrClaim(oauth, essential, acrValues);
|
openLoginFormWithAcrClaim(oauth, essential, acrValues);
|
||||||
}
|
}
|
||||||
|
@ -326,5 +377,6 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
|
||||||
private void assertErrorPage(String expectedError) {
|
private void assertErrorPage(String expectedError) {
|
||||||
Assert.assertThat(true, is(errorPage.isCurrent()));
|
Assert.assertThat(true, is(errorPage.isCurrent()));
|
||||||
Assert.assertEquals(expectedError, errorPage.getError());
|
Assert.assertEquals(expectedError, errorPage.getError());
|
||||||
|
events.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -24,10 +24,12 @@ import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
import org.keycloak.jose.jwe.JWEConstants;
|
import org.keycloak.jose.jwe.JWEConstants;
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||||
import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;
|
import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;
|
||||||
|
@ -44,6 +46,8 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
|
import org.keycloak.testsuite.forms.BrowserFlowTest;
|
||||||
|
import org.keycloak.testsuite.forms.LevelOfAssuranceFlowTest;
|
||||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||||
import org.keycloak.testsuite.util.ClientManager;
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
@ -58,6 +62,7 @@ import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -296,6 +301,57 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@AuthServerContainerExclude(REMOTE)
|
||||||
|
public void testAcrValuesSupported() throws IOException {
|
||||||
|
Client client = AdminClientUtil.createResteasyClient();
|
||||||
|
try {
|
||||||
|
// Default values when no "acr-to-loa" mapping and no authentication flow configured
|
||||||
|
OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||||
|
Assert.assertNames(oidcConfig.getAcrValuesSupported(), "0", "1");
|
||||||
|
|
||||||
|
// Update authentication flow and see it uses "acr" values from it
|
||||||
|
LevelOfAssuranceFlowTest.configureStepUpFlow(testingClient);
|
||||||
|
oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||||
|
Assert.assertNames(oidcConfig.getAcrValuesSupported(), "0", "1", "2", "3");
|
||||||
|
|
||||||
|
// Configure "ACR-To-Loa" mapping and check it has both configured values and numbers from authentication flow
|
||||||
|
RealmResource testRealm = adminClient.realm("test");
|
||||||
|
RealmRepresentation realmRep = testRealm.toRepresentation();
|
||||||
|
Map<String, Integer> acrToLoa = new HashMap<>();
|
||||||
|
acrToLoa.put("poor", 0);
|
||||||
|
acrToLoa.put("silver", 1);
|
||||||
|
acrToLoa.put("gold", 2);
|
||||||
|
String acrToLoaAttr = JsonSerialization.writeValueAsString(acrToLoa);
|
||||||
|
realmRep.getAttributes().put(Constants.ACR_LOA_MAP, acrToLoaAttr);
|
||||||
|
testRealm.update(realmRep);
|
||||||
|
|
||||||
|
oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||||
|
Assert.assertNames(oidcConfig.getAcrValuesSupported(), "poor", "silver", "gold", "0", "1", "2", "3");
|
||||||
|
|
||||||
|
// Use mappings even with values not included in the authentication flow
|
||||||
|
acrToLoa = new HashMap<>();
|
||||||
|
acrToLoa.put("poor", 0);
|
||||||
|
acrToLoa.put("silver", 1);
|
||||||
|
acrToLoa.put("gold", 2);
|
||||||
|
acrToLoa.put("platinum", 3);
|
||||||
|
acrToLoa.put("diamond", 4);
|
||||||
|
acrToLoaAttr = JsonSerialization.writeValueAsString(acrToLoa);
|
||||||
|
realmRep.getAttributes().put(Constants.ACR_LOA_MAP, acrToLoaAttr);
|
||||||
|
testRealm.update(realmRep);
|
||||||
|
|
||||||
|
oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||||
|
Assert.assertNames(oidcConfig.getAcrValuesSupported(), "poor", "silver", "gold", "platinum", "diamond", "0", "1", "2", "3");
|
||||||
|
|
||||||
|
// Revert realm and flow
|
||||||
|
realmRep.getAttributes().remove(Constants.ACR_LOA_MAP);
|
||||||
|
testRealm.update(realmRep);
|
||||||
|
BrowserFlowTest.revertFlows(testRealm, "browser - Level of Authentication FLow");
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@AuthServerContainerExclude(REMOTE)
|
@AuthServerContainerExclude(REMOTE)
|
||||||
public void testDefaultProviderCustomizations() throws IOException {
|
public void testDefaultProviderCustomizations() throws IOException {
|
||||||
|
|
|
@ -1941,7 +1941,8 @@ use-idtoken-as-detached-signature=Use ID Token as a Detached Signature
|
||||||
use-idtoken-as-detached-signature.tooltip=This makes ID token returned from Authorization Endpoint in OIDC Hybrid flow use as a detached signature defined in FAPI 1.0 Advanced Security Profile. Therefore, this ID token does not include an authenticated user's information.
|
use-idtoken-as-detached-signature.tooltip=This makes ID token returned from Authorization Endpoint in OIDC Hybrid flow use as a detached signature defined in FAPI 1.0 Advanced Security Profile. Therefore, this ID token does not include an authenticated user's information.
|
||||||
|
|
||||||
acr-loa-map=ACR to LoA Mapping
|
acr-loa-map=ACR to LoA Mapping
|
||||||
acr-loa-map.tooltip=Define which ACR (Authentication Context Class Reference) value is mapped to which LoA (Level of Authentication). The ACR can be any value, whereas the LoA must be numeric.
|
acr-loa-map.tooltip=Define which ACR (Authentication Context Class Reference) value is mapped to which LoA (Level of Authentication). The ACR can be any value, whereas the LoA must be numeric. The LoA typically refers to the numbers configured as levels in the conditions in the authentication flow. The ACR refers to the value used in the OIDC/SAML authorization request and returned to client in the tokens.
|
||||||
|
acr-loa-map-client.tooltip=Define which ACR (Authentication Context Class Reference) value is mapped to which LoA (Level of Authentication). The ACR can be any value, whereas the LoA must be numeric. This is recommended to be configured at the realm level where it is shared for all the clients. If you configure at the client level, the client mapping will take precedence over the mapping from the realm level.
|
||||||
|
|
||||||
key-not-allowed-here=Key '{{character}}' is not allowed here.
|
key-not-allowed-here=Key '{{character}}' is not allowed here.
|
||||||
|
|
||||||
|
|
|
@ -361,7 +361,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, url) {
|
function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, url, saveCallback, resetCallback) {
|
||||||
$scope.realm = angular.copy(realm);
|
$scope.realm = angular.copy(realm);
|
||||||
$scope.serverInfo = serverInfo;
|
$scope.serverInfo = serverInfo;
|
||||||
$scope.registrationAllowed = $scope.realm.registrationAllowed;
|
$scope.registrationAllowed = $scope.realm.registrationAllowed;
|
||||||
|
@ -377,6 +377,9 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $r
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
$scope.save = function() {
|
$scope.save = function() {
|
||||||
|
if (saveCallback) {
|
||||||
|
saveCallback();
|
||||||
|
}
|
||||||
var realmCopy = angular.copy($scope.realm);
|
var realmCopy = angular.copy($scope.realm);
|
||||||
console.log('updating realm...');
|
console.log('updating realm...');
|
||||||
$scope.changed = false;
|
$scope.changed = false;
|
||||||
|
@ -390,6 +393,9 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $r
|
||||||
|
|
||||||
$scope.reset = function() {
|
$scope.reset = function() {
|
||||||
$scope.realm = angular.copy(oldCopy);
|
$scope.realm = angular.copy(oldCopy);
|
||||||
|
if (resetCallback) {
|
||||||
|
resetCallback();
|
||||||
|
}
|
||||||
$scope.changed = false;
|
$scope.changed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -411,7 +417,53 @@ module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, rea
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings");
|
var resetCallback = function() {
|
||||||
|
try {
|
||||||
|
$scope.acrLoaMap = JSON.parse(realm.attributes["acr.loa.map"] || "{}");
|
||||||
|
} catch (e) {
|
||||||
|
$scope.acrLoaMap = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetCallback();
|
||||||
|
var previousNewAcr = undefined;
|
||||||
|
var previousNewLoa = undefined;
|
||||||
|
|
||||||
|
$scope.$watch('newAcr', function() {
|
||||||
|
var changed = $scope.newAcr != previousNewAcr;
|
||||||
|
if (changed) {
|
||||||
|
previousNewAcr = $scope.newAcr;
|
||||||
|
$scope.changed = true;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
$scope.$watch('newLoa', function() {
|
||||||
|
var changed = $scope.newLoa != previousNewLoa;
|
||||||
|
if (changed) {
|
||||||
|
previousNewLoa = $scope.newLoa;
|
||||||
|
$scope.changed = true;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
$scope.deleteAcrLoaMapping = function(acr) {
|
||||||
|
delete $scope.acrLoaMap[acr];
|
||||||
|
$scope.changed = true;
|
||||||
|
updateRealmAcrAttribute();
|
||||||
|
}
|
||||||
|
$scope.checkAddAcrLoaMapping = function() {
|
||||||
|
if ($scope.newAcr && $scope.newAcr.length > 0 && $scope.newLoa && $scope.newLoa.length > 0 && $scope.newLoa.match(/^[0-9]+$/)) {
|
||||||
|
console.log("Adding acrLoaMapping: " + $scope.newLoa + " : " + $scope.newAcr);
|
||||||
|
$scope.acrLoaMap[$scope.newAcr] = $scope.newLoa;
|
||||||
|
$scope.newAcr = $scope.newLoa = "";
|
||||||
|
$scope.changed = true;
|
||||||
|
updateRealmAcrAttribute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRealmAcrAttribute() {
|
||||||
|
var acrLoaMapStr = JSON.stringify($scope.acrLoaMap);
|
||||||
|
console.log("Updating realm acr.loa.map attribute: " + acrLoaMapStr);
|
||||||
|
$scope.realm.attributes["acr.loa.map"] = acrLoaMapStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings", $scope.checkAddAcrLoaMapping, resetCallback);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
||||||
|
|
|
@ -950,7 +950,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'acr-loa-map.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'acr-loa-map-client.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,26 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'sslRequired.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'sslRequired.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group clearfix block">
|
||||||
|
<label class="col-md-2 control-label" for="newAcr">{{:: 'acr-loa-map' | translate}}</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="input-group input-map" ng-repeat="(acr, loa) in acrLoaMap">
|
||||||
|
<input class="form-control" readonly value="{{acr}}">
|
||||||
|
<input class="form-control" ng-model="acrLoaMap[acr]">
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" data-ng-click="deleteAcrLoaMapping(acr)"><span class="fa fa-minus"></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-map">
|
||||||
|
<input class="form-control" ng-model="newAcr" id="newAcr" placeholder="ACR">
|
||||||
|
<input class="form-control" ng-model="newLoa" id="newLoa" placeholder="LOA">
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" data-ng-click="checkAddAcrLoaMapping()"><span class="fa fa-plus"></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'acr-loa-map.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-group" data-ng-show="access.manageRealm">
|
<div class="form-group" data-ng-show="access.manageRealm">
|
||||||
|
|
Loading…
Reference in a new issue