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:
parent
87c279d645
commit
cc78fd7ca0
4 changed files with 180 additions and 5 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTargetRoleMapper
|
|||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCUserAttributeMapper
|
||||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTypeMapper
|
||||
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.SubMapper
|
||||
org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper
|
||||
|
|
|
@ -61,6 +61,7 @@ import org.keycloak.util.JsonSerialization;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -226,7 +227,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
"sha-256",
|
||||
"did:web:issuer.org",
|
||||
2,
|
||||
List.of(),
|
||||
List.of("iat", "nbf"),
|
||||
Optional.empty(),
|
||||
VerifiableCredentialType.from("https://credentials.example.com/test-credential"),
|
||||
CredentialConfigId.from("test-credential"));
|
||||
|
@ -240,7 +241,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
"sha-256",
|
||||
"did:web:issuer.org",
|
||||
0,
|
||||
List.of("given_name"),
|
||||
List.of("given_name", "iat", "nbf"),
|
||||
Optional.empty(),
|
||||
VerifiableCredentialType.from("https://credentials.example.com/identity_credential"),
|
||||
CredentialConfigId.from("IdentityCredential"));
|
||||
|
@ -273,6 +274,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
"hashAlgorithm", List.of("sha-256"),
|
||||
"decoys", List.of("0"),
|
||||
"vct", List.of("https://credentials.example.com/identity_credential"),
|
||||
"visibleClaims", List.of("iat,nbf"),
|
||||
"vcConfigId", List.of("IdentityCredential")
|
||||
)
|
||||
));
|
||||
|
@ -293,6 +295,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
"hashAlgorithm", List.of("sha-256"),
|
||||
"decoys", List.of("2"),
|
||||
"vct", List.of("https://credentials.example.com/test-credential"),
|
||||
"visibleClaims", List.of("iat,nbf"),
|
||||
"vcConfigId", List.of("test-credential")
|
||||
)
|
||||
));
|
||||
|
@ -337,9 +340,13 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
getUserAttributeMapper("lastName", "lastName", "test-credential"),
|
||||
getIdMapper("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("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;
|
||||
|
@ -391,7 +398,8 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
|||
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");
|
||||
|
||||
}
|
||||
assertNotNull("Test credential shall include an iat claim.", jsonWebToken.getIat());
|
||||
assertNotNull("Test credential shall include an nbf claim.", jsonWebToken.getNbf()); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,10 +27,10 @@ import org.keycloak.common.util.PemUtils;
|
|||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.DefaultKeyProviders;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
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.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
|
@ -53,8 +53,10 @@ import java.security.PublicKey;
|
|||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
@ -366,4 +368,24 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
|||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue