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.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
|
||||||
|
|
|
@ -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()); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue