This commit is contained in:
Asish Kumar 2026-05-24 08:29:51 +00:00 committed by GitHub
commit 2efd4fedf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 66 additions and 7 deletions

View file

@ -18,6 +18,8 @@
package org.keycloak.protocol.oid4vc.issuance.credentialbuilder;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.UnaryOperator;
@ -71,6 +73,8 @@ public class JwtCredentialBuilder implements CredentialBuilder {
VerifiableCredential verifiableCredential,
CredentialBuildConfig credentialBuildConfig
) throws CredentialBuilderException {
verifiableCredential.setType(getCredentialTypes(verifiableCredential.getType()));
// Populate the issuer field of the VC
verifiableCredential.setIssuer(credentialBuildConfig.getCredentialIssuer());
@ -108,6 +112,14 @@ public class JwtCredentialBuilder implements CredentialBuilder {
return new JwtCredentialBody(jwsBuilder);
}
private static List<String> getCredentialTypes(List<String> credentialTypes) {
List<String> types = new ArrayList<>(Optional.ofNullable(credentialTypes).orElseGet(List::of));
if (!types.contains(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE)) {
types.add(0, CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE);
}
return types;
}
@Override
public void contributeToMetadata(SupportedCredentialConfiguration credentialConfig, CredentialScopeModel credentialScope) {
CredentialDefinition credentialDefinition = CredentialDefinition.parse(credentialScope);

View file

@ -37,14 +37,23 @@ public class CredentialDefinition {
private List<String> context;
private List<String> type = new ArrayList<>();
public static final String VERIFIABLE_CREDENTIAL_TYPE = "VerifiableCredential";
public static CredentialDefinition parse(CredentialScopeModel credentialModel) {
List<String> contexts = Optional.of(credentialModel.getVcContexts())
.filter(list -> !list.isEmpty())
.orElseGet(() -> new ArrayList<>(List.of(credentialModel.getName())));
List<String> types = Optional.ofNullable(credentialModel.getSupportedCredentialTypes())
.filter(list -> !list.isEmpty())
.map(ArrayList::new)
.orElseGet(() -> new ArrayList<>(List.of(credentialModel.getName())));
// Ensure VerifiableCredential is always included as a base type
// per the W3C Verifiable Credentials Data Model specification.
if (!types.contains(VERIFIABLE_CREDENTIAL_TYPE)) {
types.add(0, VERIFIABLE_CREDENTIAL_TYPE);
}
return new CredentialDefinition().setContext(contexts)
.setType(types);
}

View file

@ -1,6 +1,7 @@
package org.keycloak.tests.oid4vc;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -8,6 +9,7 @@ import java.util.stream.Collectors;
import org.keycloak.TokenVerifier;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.CredentialScopeRepresentation;
import org.keycloak.protocol.oid4vc.model.Proofs;
@ -141,7 +143,10 @@ public class OID4VCINaturalPersonTest extends OID4VCIssuerTestBase {
assertEquals(issuer, vcJwt.getIssuer());
Object vc = vcJwt.getOtherClaims().get("vc");
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
assertEquals(credScope.getSupportedCredentialTypes(), credential.getType());
List<String> expectedCredentialTypes = new ArrayList<>();
expectedCredentialTypes.add(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE);
expectedCredentialTypes.addAll(credScope.getSupportedCredentialTypes());
assertEquals(expectedCredentialTypes, credential.getType());
assertEquals(URI.create(issuer), credential.getIssuer());
assertEquals(expUser + "@email.cz", credential.getCredentialSubject().getClaims().get("email"));
}

View file

@ -51,6 +51,7 @@ import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCMapper;
import org.keycloak.protocol.oid4vc.model.Claim;
import org.keycloak.protocol.oid4vc.model.ClaimDisplay;
import org.keycloak.protocol.oid4vc.model.Claims;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
import org.keycloak.protocol.oid4vc.model.CredentialRequestEncryptionMetadata;
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
@ -667,9 +668,13 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerTestBase {
assertNull(supportedConfig.getVct(), "JWT_VC credentials should not have vct");
assertNotNull(supportedConfig.getCredentialDefinition());
assertNotNull(supportedConfig.getCredentialDefinition().getType());
// VerifiableCredential must always be present as a base type per W3C VC Data Model
MatcherAssert.assertThat(supportedConfig.getCredentialDefinition().getType(),
Matchers.hasItem(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE));
List<String> credentialDefinitionTypes = credScope.getSupportedCredentialTypes();
if (!credentialDefinitionTypes.isEmpty()) {
assertEquals(credentialDefinitionTypes.size(), supportedConfig.getCredentialDefinition().getType().size());
MatcherAssert.assertThat(supportedConfig.getCredentialDefinition().getType(),
Matchers.hasItems(credentialDefinitionTypes.toArray(new String[0])));
}
// @context must not be present for jwt_vc_json format per OID4VCI spec

View file

@ -21,6 +21,7 @@ import java.net.URI;
import java.util.List;
import org.keycloak.TokenVerifier;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.OID4VCAuthorizationDetail;
@ -152,7 +153,7 @@ public class OID4VCPublicClientTest extends OID4VCIssuerTestBase {
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
Object vc = jsonWebToken.getOtherClaims().get("vc");
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
assertEquals(List.of(scope), credential.getType());
assertEquals(List.of(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE, scope), credential.getType());
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
assertEquals(expUser + "@email.cz", credential.getCredentialSubject().getClaims().get("email"));
}

View file

@ -8,6 +8,7 @@ import org.keycloak.TokenVerifier;
import org.keycloak.admin.client.resource.ClientPoliciesPoliciesResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.protocol.oid4vc.clientpolicy.PredicateCredentialClientPolicy;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
@ -213,7 +214,7 @@ public class OID4VCredentialByScopeTest extends OID4VCIssuerTestBase {
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
Object vc = jsonWebToken.getOtherClaims().get("vc");
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
assertEquals(List.of(scope), credential.getType());
assertEquals(List.of(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE, scope), credential.getType());
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
assertEquals(expUser + "@email.cz", credential.getCredentialSubject().getClaims().get("email"));
}

View file

@ -4,6 +4,7 @@ import java.net.URI;
import java.util.List;
import org.keycloak.TokenVerifier;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
@ -317,7 +318,7 @@ public class OID4VCredentialOfferAuthCodeTest extends OID4VCIssuerTestBase {
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
Object vc = jsonWebToken.getOtherClaims().get("vc");
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
assertEquals(List.of(scope), credential.getType());
assertEquals(List.of(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE, scope), credential.getType());
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
assertEquals(expUser + "@email.cz", credential.getCredentialSubject().getClaims().get("email"));
}

View file

@ -30,6 +30,7 @@ import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.JwtCredentialBody;
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.JwtCredentialBuilder;
import org.keycloak.protocol.oid4vc.model.CredentialBuildConfig;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.tests.oid4vc.OID4VCIssuerTestBase;
@ -93,11 +94,33 @@ public class JwtCredentialBuilderTest extends CredentialBuilderTest {
assertEquals(expectedNormalizedNbf, payload.get("nbf").asLong());
}
@Test
public void buildJwtCredential_AddsVerifiableCredentialType() throws Exception {
VerifiableCredential verifiableCredential = getTestCredential(exampleCredentialClaims());
verifiableCredential.setType(List.of("oid4vc_natural_person"));
CredentialBuildConfig credentialBuildConfig = new CredentialBuildConfig().setTokenJwsType("JWT");
JwtCredentialBody jwtCredentialBody = builder
.buildCredentialBody(verifiableCredential, credentialBuildConfig);
String jws = jwtCredentialBody.sign(exampleSigner());
JWSInput jwsInput = new JWSInput(jws);
JsonNode credentialTypes = parseCredentialTypes(jwsInput);
assertEquals(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE, credentialTypes.get(0).asText());
assertEquals("oid4vc_natural_person", credentialTypes.get(1).asText());
}
private JsonNode parseCredentialSubject(JWSInput jwsInput) throws JWSInputException {
JsonNode payload = jwsInput.readJsonContent(JsonNode.class);
return payload.get("vc").get(CREDENTIAL_SUBJECT);
}
private JsonNode parseCredentialTypes(JWSInput jwsInput) throws JWSInputException {
JsonNode payload = jwsInput.readJsonContent(JsonNode.class);
return payload.get("vc").get("type");
}
private Map<String, Object> exampleCredentialClaims() {
return new HashMap<>(Map.of(
"id", String.format("uri:uuid:%s", UUID.randomUUID()),

View file

@ -21,6 +21,7 @@ import java.net.URI;
import java.util.List;
import org.keycloak.TokenVerifier;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
@ -98,7 +99,7 @@ public class OID4VCPublicClientPreAuthTest extends OID4VCIssuerTestBase {
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
Object vc = jsonWebToken.getOtherClaims().get("vc");
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
assertEquals(List.of(scope), credential.getType());
assertEquals(List.of(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE, scope), credential.getType());
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
assertEquals(expUser + "@email.cz", credential.getCredentialSubject().getClaims().get("email"));
}

View file

@ -5,6 +5,7 @@ import java.util.List;
import org.keycloak.TokenVerifier;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.protocol.oid4vc.model.CredentialDefinition;
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
@ -161,7 +162,7 @@ public class OID4VCredentialOfferPreAuthTest extends OID4VCIssuerTestBase {
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
Object vc = jsonWebToken.getOtherClaims().get("vc");
VerifiableCredential credential = JsonSerialization.mapper.convertValue(vc, VerifiableCredential.class);
assertEquals(List.of(scope), credential.getType());
assertEquals(List.of(CredentialDefinition.VERIFIABLE_CREDENTIAL_TYPE, scope), credential.getType());
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
assertEquals(expUser + "@email.cz", credential.getCredentialSubject().getClaims().get("email"));
}