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.
This commit is contained in:
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
|
||||
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -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…
Reference in New Issue