diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45cf2a6..cb50b21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index 513d2b8..c0fb4ef 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/pom.xml b/pom.xml
index adb00bc..a3dbe7a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,7 @@
org.keycloak
keycloak-parent
19.0.2
+
pom
import
@@ -95,6 +96,7 @@
org.slf4j
slf4j-log4j12
test
+
log4j
diff --git a/protocol-mapper/CHANGELOG.md b/protocol-mapper/CHANGELOG.md
index 0ff63e6..b03ce1b 100644
--- a/protocol-mapper/CHANGELOG.md
+++ b/protocol-mapper/CHANGELOG.md
@@ -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.
diff --git a/protocol-mapper/pom.xml b/protocol-mapper/pom.xml
index 57a72c1..732e1e0 100644
--- a/protocol-mapper/pom.xml
+++ b/protocol-mapper/pom.xml
@@ -1,4 +1,6 @@
-
+
4.0.0
@@ -12,15 +14,19 @@
jar
- scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git
- scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git
- https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}
+
+ scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git
+
+ scm:git:https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}.git
+
+ https://code-repo.d4science.org/gCubeSystem/${project.parent.artifactId}
5.8.2
3.22.0
4.5.1
+ 2.1.1
@@ -41,6 +47,12 @@
${org-mockito.version}
test
+
+
+
+
+
+
diff --git a/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java b/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java
index 74f6175..e0aa3c3 100644
--- a/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java
+++ b/protocol-mapper/src/main/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapper.java
@@ -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 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 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 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());
diff --git a/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java b/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java
index 1829ad3..0968079 100644
--- a/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java
+++ b/protocol-mapper/src/test/java/org/gcube/keycloak/protocol/oidc/mapper/D4ScienceContextMapperTest.java
@@ -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 createConfig() {
- final Map result = new HashMap<>();
- result.put("access.token.claim", "true");
- result.put("claim.name", CLAIM_NAME);
- return result;
+ private Map createConfig(boolean narrowRA) {
+ final Map 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;
}
}
\ No newline at end of file