Provided the `D4ScienceContextMapper` that maps the D4S context requested in a customizable HTTP header as token's claim having the configured name (defaulting to the 'aud' claim). Can also shrink the `resource access` token claim to have only the requested context entry.

master
Mauro Mugnaini 8 months ago
parent 2f586b695a
commit cb4d0e881c

@ -3,7 +3,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
# Changelog for "keycloak-d4science-spi-parent"
## [v2.1.0-SNAPSHOT]
- Added new [protocol-mapper](protocol-mapper/README.md) module to make the custom protocol mappers available
- Revised terms, EU links, D4Science and Blue-Cloud logo updated (#25444)
## [v2.0.0]
- Updated to be compiled/used with Keycloak v19.0.2

@ -15,6 +15,7 @@ The project is a Maven master POM project and it is composed of several modules:
* [keycloak-d4science-theme](keycloak-d4science-theme/README.md)
* [keycloak-d4science-script](keycloak-d4science-script/README.md)
* [ldap-storage-mapper](ldap-storage-mapper/README.md)
* [protocol-mapper](protocol-mapper/README.md)
## Built With

@ -42,6 +42,7 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
<version>19.0.2</version>
<!-- <version>22.0.0</version>-->
<type>pom</type>
<scope>import</scope>
</dependency>
@ -95,6 +96,7 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>test</scope>
<!-- <version>2.0.6</version>-->
</dependency>
<dependency>
<groupId>log4j</groupId>

@ -3,4 +3,4 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
# Changelog for "identity-provider-mapper"
## [v2.1.0-SNAPSHOT]
- Added new module to make the custom protocol mappers available
- Provided the `D4ScienceContextMapper` that maps the D4S context requested in a customizable HTTP header as token's claim having the configured name (defaulting to the 'aud' claim). Can also shrink the `resource access` token claim to have only the requested context entry.

@ -1,4 +1,6 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@ -12,15 +14,19 @@
<packaging>jar</packaging>
<scm>
<connection>scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git</connection>
<developerConnection>scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git</developerConnection>
<url>https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}</url>
<connection>
scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git</connection>
<developerConnection>
scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git</developerConnection>
<url>
https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}</url>
</scm>
<properties>
<junit-jupiter.version>5.8.2</junit-jupiter.version>
<assertj-core.version>3.22.0</assertj-core.version>
<org-mockito.version>4.5.1</org-mockito.version>
<javax.ws.rs.version>2.1.1</javax.ws.rs.version>
</properties>
<dependencies>
@ -41,6 +47,12 @@
<version>${org-mockito.version}</version>
<scope>test</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>javax.ws.rs</groupId>-->
<!-- <artifactId>javax.ws.rs-api</artifactId>-->
<!-- <version>${javax.ws.rs.version}</version>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
</dependencies>
<build />

@ -13,27 +13,51 @@ import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Access;
import org.keycloak.representations.IDToken;
public class D4ScienceContextMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {
private static final Logger logger = Logger.getLogger(D4ScienceContextMapper.class);
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
public static final String HTTP_REQUEST_HEADER_NAME = "d4scm.header-name";
public static final String NARROW_RESOURCE_ACCESS = "d4scm.narrow-ra";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
// Assuring that the mapper is executed as last
private static final int PRIORITY = Integer.MAX_VALUE;
private static final String DISPLAY_TYPE = "OIDC D4Science Context Mapper";
private static final String PROVIDER_ID = "oidc-d4scince-context-mapper";
public static final String HEADER_NAME = "X-D4Science-Context";
// public static final String HEADER_NAME = "X-Infrastructure-Context";
// public static final String HEADER_NAME = "X-Infra-Context";
public static final String DEFAULT_HEADER_NAME = "X-D4Science-Context";
public static final String DEFAULT_TOKEN_CLAIM = "aud";
static {
OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, D4ScienceContextMapper.class);
OIDCAttributeMapperHelper.addTokenClaimNameConfig(CONFIG_PROPERTIES);
CONFIG_PROPERTIES.forEach(conf -> {
if (OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME.equals(conf.getName()))
conf.setDefaultValue(DEFAULT_TOKEN_CLAIM);
conf.setReadOnly(true);
});
OIDCAttributeMapperHelper.addIncludeInTokensConfig(CONFIG_PROPERTIES, D4ScienceContextMapper.class);
ProviderConfigProperty headerProperty = new ProviderConfigProperty();
headerProperty.setName(HTTP_REQUEST_HEADER_NAME);
headerProperty.setLabel("HTTP request header name with the requested context");
headerProperty.setType(ProviderConfigProperty.STRING_TYPE);
headerProperty.setHelpText("The HTTP header that contains the requested context to be mapped as the requested in the configured claim");
headerProperty.setDefaultValue(DEFAULT_HEADER_NAME);
headerProperty.setReadOnly(true);
CONFIG_PROPERTIES.add(headerProperty);
ProviderConfigProperty narrowProperty = new ProviderConfigProperty();
narrowProperty.setName(NARROW_RESOURCE_ACCESS);
narrowProperty.setLabel("Narrow down resource access array?");
narrowProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE);
narrowProperty.setHelpText("Narrow down resource access claim to contain only the requested context entry");
CONFIG_PROPERTIES.add(narrowProperty);
}
@Override
@ -53,12 +77,12 @@ public class D4ScienceContextMapper extends AbstractOIDCProtocolMapper implement
@Override
public String getHelpText() {
return "Maps the D4Science context audience by reading the '" + HEADER_NAME + "' header and sets it as the configured token claim";
return "Maps the D4Science context audience by reading the configured header's value and sets it as the configured token claim, if it is in scope";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
return CONFIG_PROPERTIES;
}
@Override
@ -76,17 +100,28 @@ public class D4ScienceContextMapper extends AbstractOIDCProtocolMapper implement
// Since only the OIDCAccessTokenMapper interface is implemented, we are almost sure that
// the token object is an AccessToken but adding a specific check anyway
if (token instanceof AccessToken) {
logger.debugf("Looking for the '%s' header", HEADER_NAME);
String requestedD4SContext = keycloakSession.getContext().getRequestHeaders().getHeaderString(HEADER_NAME);
AccessToken accessToken = ((AccessToken) token);
String headerName = mappingModel.getConfig().get(HTTP_REQUEST_HEADER_NAME);
if (headerName == null || "".equals(headerName)) {
headerName = DEFAULT_HEADER_NAME;
}
logger.debugf("Looking for the '%s' header", headerName);
String requestedD4SContext = keycloakSession.getContext().getRequestHeaders().getHeaderString(headerName);
if (requestedD4SContext != null && !"".equals(requestedD4SContext)) {
logger.debugf("Checking resource access for the requested context: %s", requestedD4SContext);
if (((AccessToken) token).getResourceAccess().containsKey(requestedD4SContext)) {
Access contextAccessInResourceAccess = accessToken.getResourceAccess(requestedD4SContext);
if (contextAccessInResourceAccess != null) {
logger.debugf("Mapping it as the configured claim: %s",
mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME));
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, requestedD4SContext);
if (Boolean.parseBoolean(mappingModel.getConfig().get(NARROW_RESOURCE_ACCESS))) {
// Removing all access details but the requested context
accessToken.getResourceAccess().clear();
accessToken.getResourceAccess().put(requestedD4SContext, contextAccessInResourceAccess);
}
} else {
logger.warnf("Requested context '%s' is not accessible to the client: %s", requestedD4SContext,
clientSessionCtx.getClientSession().getClient().getName());

@ -6,11 +6,11 @@ import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.ws.rs.core.HttpHeaders;
import org.assertj.core.util.Maps;
import org.junit.Test;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@ -31,7 +31,6 @@ import org.mockito.Mockito;
*/
public class D4ScienceContextMapperTest {
static final String CLAIM_NAME = "haandlerIdClaimNameExample";
static final String HEADER_VALUE = "ginostilla";
@Test
@ -62,31 +61,44 @@ public class D4ScienceContextMapperTest {
.collect(Collectors.toList());
assertThat(configPropertyNames).containsExactly(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME,
OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, D4ScienceContextMapper.HTTP_REQUEST_HEADER_NAME,
D4ScienceContextMapper.NARROW_RESOURCE_ACCESS);
}
@Test
public void shouldAddClaim() {
public void shouldAddClaimAndNotNarrow() {
final UserSessionModel session = givenUserSession();
final KeycloakSession keycloakSession = givenKeycloakSession(true);
final AccessToken accessToken = transformAccessToken(session, keycloakSession, true);
assertThat(accessToken.getOtherClaims().get(CLAIM_NAME)).isEqualTo(HEADER_VALUE);
final AccessToken accessToken = transformAccessToken(session, keycloakSession, true, false);
assertThat(accessToken.getAudience()[0]).isEqualTo(HEADER_VALUE);
assertThat(accessToken.getResourceAccess().size()).isEqualTo(2);
assertThat(accessToken.getResourceAccess().keySet()).contains(HEADER_VALUE);
}
@Test
public void shouldAddClaimAndNarrow() {
final UserSessionModel session = givenUserSession();
final KeycloakSession keycloakSession = givenKeycloakSession(true);
final AccessToken accessToken = transformAccessToken(session, keycloakSession, true, true);
assertThat(accessToken.getAudience()[0]).isEqualTo(HEADER_VALUE);
assertThat(accessToken.getResourceAccess().size()).isEqualTo(1);
assertThat(accessToken.getResourceAccess().keySet().iterator().next()).isEqualTo(HEADER_VALUE);
}
@Test
public void shouldNotAddClaim() {
final UserSessionModel session = givenUserSession();
final KeycloakSession keycloakSession = givenKeycloakSession(false);
final AccessToken accessToken = transformAccessToken(session, keycloakSession, true);
assertThat(accessToken.getOtherClaims().get(CLAIM_NAME)).isNull();
final AccessToken accessToken = transformAccessToken(session, keycloakSession, true, false);
assertThat(accessToken.getAudience()).isNull();
}
@Test
public void shouldNotAddClaimAndLogWarning() {
final UserSessionModel session = givenUserSession();
final KeycloakSession keycloakSession = givenKeycloakSession(true);
final AccessToken accessToken = transformAccessToken(session, keycloakSession, false);
assertThat(accessToken.getOtherClaims().get(CLAIM_NAME)).isNull();
final AccessToken accessToken = transformAccessToken(session, keycloakSession, false, false);
assertThat(accessToken.getAudience()).isNull();
}
private UserSessionModel givenUserSession() {
@ -104,21 +116,22 @@ public class D4ScienceContextMapperTest {
when(context.getRequestHeaders()).thenReturn(headers);
if (withHeader) {
when(headers.getHeaderString(D4ScienceContextMapper.HEADER_NAME)).thenReturn(HEADER_VALUE);
when(headers.getHeaderString(D4ScienceContextMapper.DEFAULT_HEADER_NAME)).thenReturn(HEADER_VALUE);
} else {
when(headers.getHeaderString(D4ScienceContextMapper.HEADER_NAME)).thenReturn("");
when(headers.getHeaderString(D4ScienceContextMapper.DEFAULT_HEADER_NAME)).thenReturn("");
}
return keycloakSession;
}
private AccessToken transformAccessToken(UserSessionModel userSessionModel, KeycloakSession keycloakSession,
boolean withResourceAccess) {
boolean withResourceAccess, boolean withNarrowRA) {
final ProtocolMapperModel mappingModel = new ProtocolMapperModel();
mappingModel.setConfig(createConfig());
mappingModel.setConfig(createConfig(withNarrowRA));
AccessToken at = new AccessToken();
if (withResourceAccess) {
at.setResourceAccess(Maps.newHashMap(HEADER_VALUE, null));
at.addAccess(HEADER_VALUE);
at.addAccess(UUID.randomUUID().toString());
}
return new D4ScienceContextMapper().transformAccessToken(at, mappingModel, keycloakSession,
@ -135,11 +148,13 @@ public class D4ScienceContextMapperTest {
return csc;
}
private Map<String, String> createConfig() {
final Map<String, String> result = new HashMap<>();
result.put("access.token.claim", "true");
result.put("claim.name", CLAIM_NAME);
return result;
private Map<String, String> createConfig(boolean narrowRA) {
final Map<String, String> config = new HashMap<>();
config.put(D4ScienceContextMapper.HTTP_REQUEST_HEADER_NAME, D4ScienceContextMapper.DEFAULT_HEADER_NAME);
config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, D4ScienceContextMapper.DEFAULT_TOKEN_CLAIM);
config.put(D4ScienceContextMapper.NARROW_RESOURCE_ACCESS, Boolean.toString(narrowRA));
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
return config;
}
}
Loading…
Cancel
Save