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:
Marek Posolda 2022-02-18 11:33:31 +01:00 committed by GitHub
parent f2ed799b8b
commit caf37b1f70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 340 additions and 36 deletions

View file

@ -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;
} }

View file

@ -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);
}
}
} }

View file

@ -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);
} }

View file

@ -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();
} }
} }

View file

@ -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,18 +85,26 @@ 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)
@ -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();
} }
} }

View file

@ -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 {

View file

@ -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.

View file

@ -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) {

View file

@ -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>

View file

@ -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">