Provided keycloak with a protocol mapper, that can allow to optionally add iat and nbf claims to VCs (#31620)

closes #31581 


Signed-off-by: Francis Pouatcha <francis.pouatcha@adorsys.com>
This commit is contained in:
Francis Pouatcha 2024-07-29 09:32:48 +02:00 committed by GitHub
parent 87c279d645
commit cc78fd7ca0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 180 additions and 5 deletions

View file

@ -0,0 +1,144 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oid4vc.issuance.mappers;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.provider.ProviderConfigProperty;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* Map issuance date to the credential, under the default claim name "iat"
* <p>
* subjectProperty can be used to change the claim name.
* <p>
* Source of the information can either be computed, or read from the VerifiableCredential object
* bearing other claims. Default is the value in the verifiable credential.
* <p>
* We will use the java.time.temporal.ChronoUnit enum values to help flatten down the time.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class OID4VCIssuedAtTimeClaimMapper extends OID4VCMapper {
public static final String MAPPER_ID = "oid4vc-issued-at-time-claim-mapper";
// Omit value if defaults to "iat"
public static final String SUBJECT_PROPERTY_CONFIG_KEY = "subjectProperty";
// We will use the java.time.temporal.ChronoUnit enum values to help flatten down the time.
// Omit property if no truncation.
public static final String TRUNCATE_TO_TIME_UNIT_KEY = "truncateToTimeUnit";
// Time computed (COMPUTE) or taken from the verifiable credential (VC).
// Defaults to VC. Falls back to COMPUTE.
public static final String VALUE_SOURCE = "valueSource";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
static {
ProviderConfigProperty subjectPropertyNameConfig = new ProviderConfigProperty();
subjectPropertyNameConfig.setName(SUBJECT_PROPERTY_CONFIG_KEY);
subjectPropertyNameConfig.setLabel("Time Claim Name");
subjectPropertyNameConfig.setHelpText("Name of this time claim. Default is iat");
subjectPropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
subjectPropertyNameConfig.setDefaultValue("iat");
CONFIG_PROPERTIES.add(subjectPropertyNameConfig);
ProviderConfigProperty truncateToTimeUnit = new ProviderConfigProperty();
truncateToTimeUnit.setName(TRUNCATE_TO_TIME_UNIT_KEY);
truncateToTimeUnit.setLabel("Truncate To Time Unit");
truncateToTimeUnit.setHelpText("Truncate time to the first second of the MINUTES, HOURS, HALF_DAYS, DAYS, WEEKS, MONTHS or YEARS. Such as to prevent correlation of credentials based on this time value.");
truncateToTimeUnit.setType(ProviderConfigProperty.LIST_TYPE);
truncateToTimeUnit.setOptions(List.of("MINUTES", "HOURS", "HALF_DAYS", "DAYS", "WEEKS", "MONTHS", "YEARS"));
CONFIG_PROPERTIES.add(truncateToTimeUnit);
ProviderConfigProperty valueSource = new ProviderConfigProperty();
valueSource.setName(VALUE_SOURCE);
valueSource.setLabel("Source of Value");
valueSource.setHelpText("Tells the protocol mapper where to get the information. For now: COMPUTE or VC. Default is COMPUTE, in which this protocol mapper computes the current time in seconds. With value `VC`, the time is read from the verifiable credential issuance date field.");
valueSource.setType(ProviderConfigProperty.LIST_TYPE);
valueSource.setOptions(List.of("COMPUTE", "VC"));
valueSource.setDefaultValue("COMPUTE");
CONFIG_PROPERTIES.add(valueSource);
}
@Override
protected List<ProviderConfigProperty> getIndividualConfigProperties() {
return CONFIG_PROPERTIES;
}
public void setClaimsForCredential(VerifiableCredential verifiableCredential,
UserSessionModel userSessionModel) {
Instant iat = Optional.ofNullable(mapperModel.getConfig())
.flatMap(config -> Optional.ofNullable(config.get(VALUE_SOURCE)))
.filter(valueSource -> Objects.equals(valueSource, "COMPUTE"))
.map(valueSource -> Instant.now())
.orElseGet(() -> Optional.ofNullable(verifiableCredential.getIssuanceDate())
.orElse(Instant.now()));
// truncate is possible. Return iat if not.
Instant iatTrunc = Optional.ofNullable(mapperModel.getConfig())
.flatMap(config -> Optional.ofNullable(config.get(TRUNCATE_TO_TIME_UNIT_KEY)))
.filter(i -> i.isEmpty())
.map(ChronoUnit::valueOf)
.map(iat::truncatedTo)
.orElse(iat);
// Set the value
String propertyName = Optional.ofNullable(mapperModel.getConfig())
.map(config -> config.get(SUBJECT_PROPERTY_CONFIG_KEY))
.orElse("iat");
CredentialSubject credentialSubject = verifiableCredential.getCredentialSubject();
credentialSubject.setClaims(propertyName, iatTrunc.getEpochSecond());
}
@Override
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
// NoOp
}
@Override
public String getDisplayType() {
return "Issuance Date Claim Mapper";
}
@Override
public String getHelpText() {
return "Allows to set the issuance date credential subject.";
}
@Override
public ProtocolMapper create(KeycloakSession session) {
return new OID4VCIssuedAtTimeClaimMapper();
}
@Override
public String getId() {
return MAPPER_ID;
}
}

View file

@ -54,6 +54,7 @@ org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTargetRoleMapper
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCUserAttributeMapper org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCUserAttributeMapper
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTypeMapper org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTypeMapper
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCContextMapper org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCContextMapper
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCIssuedAtTimeClaimMapper
org.keycloak.protocol.oidc.mappers.SessionStateMapper org.keycloak.protocol.oidc.mappers.SessionStateMapper
org.keycloak.protocol.oidc.mappers.SubMapper org.keycloak.protocol.oidc.mappers.SubMapper
org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper

View file

@ -61,6 +61,7 @@ import org.keycloak.util.JsonSerialization;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.temporal.ChronoUnit;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -226,7 +227,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
"sha-256", "sha-256",
"did:web:issuer.org", "did:web:issuer.org",
2, 2,
List.of(), List.of("iat", "nbf"),
Optional.empty(), Optional.empty(),
VerifiableCredentialType.from("https://credentials.example.com/test-credential"), VerifiableCredentialType.from("https://credentials.example.com/test-credential"),
CredentialConfigId.from("test-credential")); CredentialConfigId.from("test-credential"));
@ -240,7 +241,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
"sha-256", "sha-256",
"did:web:issuer.org", "did:web:issuer.org",
0, 0,
List.of("given_name"), List.of("given_name", "iat", "nbf"),
Optional.empty(), Optional.empty(),
VerifiableCredentialType.from("https://credentials.example.com/identity_credential"), VerifiableCredentialType.from("https://credentials.example.com/identity_credential"),
CredentialConfigId.from("IdentityCredential")); CredentialConfigId.from("IdentityCredential"));
@ -273,6 +274,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
"hashAlgorithm", List.of("sha-256"), "hashAlgorithm", List.of("sha-256"),
"decoys", List.of("0"), "decoys", List.of("0"),
"vct", List.of("https://credentials.example.com/identity_credential"), "vct", List.of("https://credentials.example.com/identity_credential"),
"visibleClaims", List.of("iat,nbf"),
"vcConfigId", List.of("IdentityCredential") "vcConfigId", List.of("IdentityCredential")
) )
)); ));
@ -293,6 +295,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
"hashAlgorithm", List.of("sha-256"), "hashAlgorithm", List.of("sha-256"),
"decoys", List.of("2"), "decoys", List.of("2"),
"vct", List.of("https://credentials.example.com/test-credential"), "vct", List.of("https://credentials.example.com/test-credential"),
"visibleClaims", List.of("iat,nbf"),
"vcConfigId", List.of("test-credential") "vcConfigId", List.of("test-credential")
) )
)); ));
@ -337,9 +340,13 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
getUserAttributeMapper("lastName", "lastName", "test-credential"), getUserAttributeMapper("lastName", "lastName", "test-credential"),
getIdMapper("test-credential"), getIdMapper("test-credential"),
getStaticClaimMapper("test-credential", "test-credential"), getStaticClaimMapper("test-credential", "test-credential"),
getIssuedAtTimeMapper(null, ChronoUnit.HOURS.name(), "COMPUTE","test-credential"),
getIssuedAtTimeMapper("nbf", null, "COMPUTE","test-credential"),
getUserAttributeMapper("given_name", "firstName", "identity_credential"), getUserAttributeMapper("given_name", "firstName", "identity_credential"),
getUserAttributeMapper("family_name", "lastName", "identity_credential") getUserAttributeMapper("family_name", "lastName", "identity_credential"),
getIssuedAtTimeMapper(null, ChronoUnit.MINUTES.name(), "COMPUTE","identity_credential"),
getIssuedAtTimeMapper("nbf", ChronoUnit.SECONDS.name(), "COMPUTE","identity_credential")
) )
); );
return clientRepresentation; return clientRepresentation;
@ -391,7 +398,8 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
assertTrue("The credentials should include the email claim.", disclosureMap.containsKey("email")); assertTrue("The credentials should include the email claim.", disclosureMap.containsKey("email"));
assertEquals("email claim incorrectly mapped.", disclosureMap.get("email").get(2).asText(), "john@email.cz"); assertEquals("email claim incorrectly mapped.", disclosureMap.get("email").get(2).asText(), "john@email.cz");
} assertNotNull("Test credential shall include an iat claim.", jsonWebToken.getIat());
assertNotNull("Test credential shall include an nbf claim.", jsonWebToken.getNbf()); }
} }
} }

View file

@ -27,10 +27,10 @@ import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.DefaultKeyProviders;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory; import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
import org.keycloak.protocol.oid4vc.issuance.TimeProvider; import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCIssuedAtTimeClaimMapper;
import org.keycloak.protocol.oid4vc.model.CredentialSubject; import org.keycloak.protocol.oid4vc.model.CredentialSubject;
import org.keycloak.protocol.oid4vc.model.Format; import org.keycloak.protocol.oid4vc.model.Format;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential; import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
@ -53,8 +53,10 @@ import java.security.PublicKey;
import java.security.Security; import java.security.Security;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
/** /**
@ -366,4 +368,24 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
); );
return protocolMapperRepresentation; return protocolMapperRepresentation;
} }
protected ProtocolMapperRepresentation getIssuedAtTimeMapper(String subjectProperty, String truncateToTimeUnit, String valueSource, String supportedCredentialTypes) {
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
protocolMapperRepresentation.setName(supportedCredentialTypes + "-" + subjectProperty + "-oid4vc-issued-at-time-claim-mapper");
protocolMapperRepresentation.setProtocol("oid4vc");
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
protocolMapperRepresentation.setProtocolMapper("oid4vc-issued-at-time-claim-mapper");
Map<String, String> configMap = new HashMap<>();
configMap.put("supportedCredentialTypes", supportedCredentialTypes);
Optional.ofNullable(subjectProperty)
.ifPresent(value -> configMap.put(OID4VCIssuedAtTimeClaimMapper.SUBJECT_PROPERTY_CONFIG_KEY, value));
Optional.ofNullable(truncateToTimeUnit)
.ifPresent(value -> configMap.put(OID4VCIssuedAtTimeClaimMapper.TRUNCATE_TO_TIME_UNIT_KEY, value));
Optional.ofNullable(valueSource)
.ifPresent(value -> configMap.put(OID4VCIssuedAtTimeClaimMapper.VALUE_SOURCE, value));
protocolMapperRepresentation.setConfig(configMap);
return protocolMapperRepresentation;
}
} }