mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Restructure credential_configurations_supported parsing to handle credential_metadata with display and claims && Update Credential Issuer Metadata structure (#42001)
Closes #41587 Closes #41597 Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
parent
320ea5a9a7
commit
d5feb76f1f
@ -32,15 +32,20 @@ import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequestEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSServerUtils;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.urls.UrlType;
|
||||
import org.keycloak.wellknown.WellKnownProvider;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.keycloak.crypto.KeyType.RSA;
|
||||
|
||||
@ -81,8 +86,8 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
.setCredentialsSupported(getSupportedCredentials(keycloakSession))
|
||||
.setAuthorizationServers(List.of(getIssuer(context)))
|
||||
.setCredentialResponseEncryption(getCredentialResponseEncryption(keycloakSession))
|
||||
.setBatchCredentialIssuance(getBatchCredentialIssuance(keycloakSession))
|
||||
.setSignedMetadata(getSignedMetadata(keycloakSession));
|
||||
.setCredentialRequestEncryption(getCredentialRequestEncryption(keycloakSession))
|
||||
.setBatchCredentialIssuance(getBatchCredentialIssuance(keycloakSession));
|
||||
return issuer;
|
||||
}
|
||||
|
||||
@ -119,11 +124,6 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getSignedMetadata(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
return realm.getAttribute("signed_metadata");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the credential response encryption высоко for the issuer.
|
||||
* Now determines supported algorithms from available realm keys.
|
||||
@ -138,6 +138,27 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
// Get supported algorithms from available encryption keys
|
||||
metadata.setAlgValuesSupported(getSupportedEncryptionAlgorithms(session));
|
||||
metadata.setEncValuesSupported(getSupportedEncryptionMethods());
|
||||
metadata.setZipValuesSupported(getSupportedCompressionMethods());
|
||||
metadata.setEncryptionRequired(isEncryptionRequired(realm));
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the credential request encryption metadata for the issuer.
|
||||
* Determines supported algorithms from available realm keys.
|
||||
*
|
||||
* @param session The Keycloak session
|
||||
* @return The credential request encryption metadata
|
||||
*/
|
||||
public static CredentialRequestEncryptionMetadata getCredentialRequestEncryption(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
CredentialRequestEncryptionMetadata metadata = new CredentialRequestEncryptionMetadata();
|
||||
|
||||
// Get supported algorithms from available encryption keys
|
||||
metadata.setJwks(getEncryptionJwks(session));
|
||||
metadata.setEncValuesSupported(getSupportedEncryptionMethods());
|
||||
metadata.setZipValuesSupported(getSupportedCompressionMethods());
|
||||
metadata.setEncryptionRequired(isEncryptionRequired(realm));
|
||||
|
||||
return metadata;
|
||||
@ -178,6 +199,37 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
return List.of(JWEConstants.A256GCM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the supported compression methods from realm attributes.
|
||||
*
|
||||
* Note: Keycloak's JWE implementation currently only has placeholder support for compression
|
||||
* in the JWEHeader class, but no actual compression/decompression logic is implemented.
|
||||
* The compression algorithm field exists but is not processed during JWE encoding/decoding.
|
||||
*
|
||||
* TODO: Implement JWE compression support when Keycloak core adds compression functionality
|
||||
*/
|
||||
private static List<String> getSupportedCompressionMethods() {
|
||||
// Keycloak JWE implementation lacks compression support - only header placeholder exists
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encryption JWKS from realm keys.
|
||||
* Filters the realm JWKS to include only encryption keys.
|
||||
*/
|
||||
private static List<JWK> getEncryptionJwks(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
JSONWebKeySet realmJwks = JWKSServerUtils.getRealmJwks(session, realm);
|
||||
|
||||
if (realmJwks.getKeys() == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return Stream.of(realmJwks.getKeys())
|
||||
.filter(jwk -> KeyUse.ENC.getSpecName().equals(jwk.getPublicKeyUse()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether encryption is required from realm attributes.
|
||||
*/
|
||||
|
||||
@ -55,9 +55,6 @@ public class CredentialIssuer {
|
||||
@JsonProperty("batch_credential_issuance")
|
||||
private BatchCredentialIssuance batchCredentialIssuance;
|
||||
|
||||
@JsonProperty("signed_metadata")
|
||||
private String signedMetadata;
|
||||
|
||||
@JsonProperty("credential_configurations_supported")
|
||||
private Map<String, SupportedCredentialConfiguration> credentialsSupported;
|
||||
|
||||
@ -67,6 +64,9 @@ public class CredentialIssuer {
|
||||
@JsonProperty("credential_response_encryption")
|
||||
private CredentialResponseEncryptionMetadata credentialResponseEncryption;
|
||||
|
||||
@JsonProperty("credential_request_encryption")
|
||||
private CredentialRequestEncryptionMetadata credentialRequestEncryption;
|
||||
|
||||
public String getCredentialIssuer() {
|
||||
return credentialIssuer;
|
||||
}
|
||||
@ -130,15 +130,6 @@ public class CredentialIssuer {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getSignedMetadata() {
|
||||
return signedMetadata;
|
||||
}
|
||||
|
||||
public CredentialIssuer setSignedMetadata(String signedMetadata) {
|
||||
this.signedMetadata = signedMetadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, SupportedCredentialConfiguration> getCredentialsSupported() {
|
||||
return credentialsSupported;
|
||||
}
|
||||
@ -169,6 +160,15 @@ public class CredentialIssuer {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialRequestEncryptionMetadata getCredentialRequestEncryption() {
|
||||
return credentialRequestEncryption;
|
||||
}
|
||||
|
||||
public CredentialIssuer setCredentialRequestEncryption(CredentialRequestEncryptionMetadata credentialRequestEncryption) {
|
||||
this.credentialRequestEncryption = credentialRequestEncryption;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the batch_credential_issuance metadata parameter.
|
||||
*/
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 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.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents credential_metadata as defined in the OID4VCI specification.
|
||||
* Contains information relevant to the usage and display of issued Credentials.
|
||||
* Format-specific mechanisms can overwrite the information in this object.
|
||||
*
|
||||
* @author <a href="https://github.com/forkimenjeckayang">Forkim Akwichek</a>
|
||||
* @see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html#name-credential-issuer-metadata-p
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialMetadata {
|
||||
|
||||
@JsonProperty("display")
|
||||
private List<DisplayObject> display;
|
||||
|
||||
@JsonProperty("claims")
|
||||
private Claims claims;
|
||||
|
||||
/**
|
||||
* Parse credential metadata from a credential scope model.
|
||||
* Format-specific mechanisms (like SD-JWT VC display metadata) are always preferred by the Wallet
|
||||
* over the information in this object, which serves as the default fallback.
|
||||
*
|
||||
* @param keycloakSession The Keycloak session
|
||||
* @param credentialScope The credential scope model
|
||||
* @return The parsed credential metadata, or null if no metadata is available
|
||||
*/
|
||||
public static CredentialMetadata parse(KeycloakSession keycloakSession, CredentialScopeModel credentialScope) {
|
||||
CredentialMetadata metadata = new CredentialMetadata();
|
||||
|
||||
// Parse format-specific display metadata (prioritized)
|
||||
List<DisplayObject> formatSpecificDisplay = DisplayObject.parse(credentialScope);
|
||||
if (formatSpecificDisplay != null && !formatSpecificDisplay.isEmpty()) {
|
||||
metadata.setDisplay(formatSpecificDisplay);
|
||||
}
|
||||
|
||||
// Parse format-specific claims metadata (prioritized)
|
||||
Claims formatSpecificClaims = Claims.parse(keycloakSession, credentialScope);
|
||||
if (formatSpecificClaims != null && !formatSpecificClaims.isEmpty()) {
|
||||
metadata.setClaims(formatSpecificClaims);
|
||||
}
|
||||
|
||||
// Only return metadata if we have some content
|
||||
if (metadata.getDisplay() != null || metadata.getClaims() != null) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<DisplayObject> getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
public CredentialMetadata setDisplay(List<DisplayObject> display) {
|
||||
this.display = display;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Claims getClaims() {
|
||||
return claims;
|
||||
}
|
||||
|
||||
public CredentialMetadata setClaims(Claims claims) {
|
||||
this.claims = claims;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 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.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the credential_request_encryption metadata for an OID4VCI Credential Issuer.
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html#name-credential-issuer-metadata-p}
|
||||
*
|
||||
* @author <a href="https://github.com/forkimenjeckayang">Forkim Akwichek</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialRequestEncryptionMetadata {
|
||||
|
||||
@JsonProperty("jwks")
|
||||
private List<JWK> jwks;
|
||||
|
||||
@JsonProperty("enc_values_supported")
|
||||
private List<String> encValuesSupported;
|
||||
|
||||
@JsonProperty("zip_values_supported")
|
||||
private List<String> zipValuesSupported;
|
||||
|
||||
@JsonProperty("encryption_required")
|
||||
private Boolean encryptionRequired;
|
||||
|
||||
public List<JWK> getJwks() {
|
||||
return jwks;
|
||||
}
|
||||
|
||||
public CredentialRequestEncryptionMetadata setJwks(List<JWK> jwks) {
|
||||
this.jwks = jwks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getEncValuesSupported() {
|
||||
return encValuesSupported;
|
||||
}
|
||||
|
||||
public CredentialRequestEncryptionMetadata setEncValuesSupported(List<String> encValuesSupported) {
|
||||
this.encValuesSupported = encValuesSupported;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getZipValuesSupported() {
|
||||
return zipValuesSupported;
|
||||
}
|
||||
|
||||
public CredentialRequestEncryptionMetadata setZipValuesSupported(List<String> zipValuesSupported) {
|
||||
this.zipValuesSupported = zipValuesSupported;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Boolean getEncryptionRequired() {
|
||||
return encryptionRequired;
|
||||
}
|
||||
|
||||
public CredentialRequestEncryptionMetadata setEncryptionRequired(Boolean encryptionRequired) {
|
||||
this.encryptionRequired = encryptionRequired;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the credential_response_encryption metadata for an OID4VCI Credential Issuer.
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-15.html#name-credential-issuer-metadata-p}
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html#name-credential-issuer-metadata-p}
|
||||
*
|
||||
* @author <a href="mailto:Bertrand.Ogen@adorsys.com">Bertrand Ogen</a>
|
||||
*/
|
||||
@ -37,6 +37,9 @@ public class CredentialResponseEncryptionMetadata {
|
||||
@JsonProperty("enc_values_supported")
|
||||
private List<String> encValuesSupported;
|
||||
|
||||
@JsonProperty("zip_values_supported")
|
||||
private List<String> zipValuesSupported;
|
||||
|
||||
@JsonProperty("encryption_required")
|
||||
private Boolean encryptionRequired;
|
||||
|
||||
@ -56,6 +59,14 @@ public class CredentialResponseEncryptionMetadata {
|
||||
this.encValuesSupported = encValuesSupported;
|
||||
}
|
||||
|
||||
public List<String> getZipValuesSupported() {
|
||||
return zipValuesSupported;
|
||||
}
|
||||
|
||||
public void setZipValuesSupported(List<String> zipValuesSupported) {
|
||||
this.zipValuesSupported = zipValuesSupported;
|
||||
}
|
||||
|
||||
public Boolean getEncryptionRequired() {
|
||||
return encryptionRequired;
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a DisplayObject, as used in the OID4VCI Credentials Issuer Metadata
|
||||
@ -59,6 +60,8 @@ public class DisplayObject {
|
||||
private static final String BG_COLOR_KEY = "background_color";
|
||||
@JsonIgnore
|
||||
private static final String TEXT_COLOR_KEY = "text_color";
|
||||
@JsonIgnore
|
||||
private static final String BG_IMAGE_KEY = "background_image";
|
||||
|
||||
@JsonProperty(DisplayObject.NAME_KEY)
|
||||
private String name;
|
||||
@ -67,7 +70,7 @@ public class DisplayObject {
|
||||
private String locale;
|
||||
|
||||
@JsonProperty(DisplayObject.LOGO_KEY)
|
||||
private String logo;
|
||||
private LogoObject logo;
|
||||
|
||||
@JsonProperty(DisplayObject.DESCRIPTION_KEY)
|
||||
private String description;
|
||||
@ -78,6 +81,9 @@ public class DisplayObject {
|
||||
@JsonProperty(DisplayObject.TEXT_COLOR_KEY)
|
||||
private String textColor;
|
||||
|
||||
@JsonProperty(DisplayObject.BG_IMAGE_KEY)
|
||||
private BackgroundImageObject backgroundImage;
|
||||
|
||||
public static List<DisplayObject> parse(CredentialScopeModel credentialScope) {
|
||||
String display = credentialScope.getVcDisplay();
|
||||
if (StringUtil.isBlank(display)) {
|
||||
@ -114,11 +120,11 @@ public class DisplayObject {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getLogo() {
|
||||
public LogoObject getLogo() {
|
||||
return logo;
|
||||
}
|
||||
|
||||
public DisplayObject setLogo(String logo) {
|
||||
public DisplayObject setLogo(LogoObject logo) {
|
||||
this.logo = logo;
|
||||
return this;
|
||||
}
|
||||
@ -150,6 +156,15 @@ public class DisplayObject {
|
||||
return this;
|
||||
}
|
||||
|
||||
public BackgroundImageObject getBackgroundImage() {
|
||||
return backgroundImage;
|
||||
}
|
||||
|
||||
public DisplayObject setBackgroundImage(BackgroundImageObject backgroundImage) {
|
||||
this.backgroundImage = backgroundImage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String toJsonString(){
|
||||
try {
|
||||
return JsonSerialization.writeValueAsString(this);
|
||||
@ -178,6 +193,8 @@ public class DisplayObject {
|
||||
return false;
|
||||
if (getBackgroundColor() != null ? !getBackgroundColor().equals(that.getBackgroundColor()) : that.getBackgroundColor() != null)
|
||||
return false;
|
||||
if (getBackgroundImage() != null ? !getBackgroundImage().equals(that.getBackgroundImage()) : that.getBackgroundImage() != null)
|
||||
return false;
|
||||
return getTextColor() != null ? getTextColor().equals(that.getTextColor()) : that.getTextColor() == null;
|
||||
}
|
||||
|
||||
@ -188,6 +205,7 @@ public class DisplayObject {
|
||||
result = 31 * result + (getLogo() != null ? getLogo().hashCode() : 0);
|
||||
result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0);
|
||||
result = 31 * result + (getBackgroundColor() != null ? getBackgroundColor().hashCode() : 0);
|
||||
result = 31 * result + (getBackgroundImage() != null ? getBackgroundImage().hashCode() : 0);
|
||||
result = 31 * result + (getTextColor() != null ? getTextColor().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
@ -200,4 +218,78 @@ public class DisplayObject {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a logo object as defined in the OID4VCI specification.
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p}
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class LogoObject {
|
||||
@JsonProperty("uri")
|
||||
private String uri;
|
||||
|
||||
@JsonProperty("alt_text")
|
||||
private String altText;
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public LogoObject setUri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAltText() {
|
||||
return altText;
|
||||
}
|
||||
|
||||
public LogoObject setAltText(String altText) {
|
||||
this.altText = altText;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof LogoObject that)) return false;
|
||||
return Objects.equals(uri, that.uri) && Objects.equals(altText, that.altText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(uri, altText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a background image object as defined in the OID4VCI specification.
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p}
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class BackgroundImageObject {
|
||||
@JsonProperty("uri")
|
||||
private String uri;
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public BackgroundImageObject setUri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof BackgroundImageObject that)) return false;
|
||||
return Objects.equals(uri, that.uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,17 +46,15 @@ public class SupportedCredentialConfiguration {
|
||||
@JsonIgnore
|
||||
private static final String CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY = "credential_signing_alg_values_supported";
|
||||
@JsonIgnore
|
||||
private static final String DISPLAY_KEY = "display";
|
||||
@JsonIgnore
|
||||
private static final String PROOF_TYPES_SUPPORTED_KEY = "proof_types_supported";
|
||||
@JsonIgnore
|
||||
private static final String CLAIMS_KEY = "claims";
|
||||
@JsonIgnore
|
||||
public static final String VERIFIABLE_CREDENTIAL_TYPE_KEY = "vct";
|
||||
@JsonIgnore
|
||||
private static final String CREDENTIAL_DEFINITION_KEY = "credential_definition";
|
||||
@JsonIgnore
|
||||
public static final String CREDENTIAL_BUILD_CONFIG_KEY = "credential_build_config";
|
||||
@JsonIgnore
|
||||
private static final String CREDENTIAL_METADATA_KEY = "credential_metadata";
|
||||
|
||||
private String id;
|
||||
|
||||
@ -72,9 +70,6 @@ public class SupportedCredentialConfiguration {
|
||||
@JsonProperty(CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY)
|
||||
private List<String> credentialSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty(DISPLAY_KEY)
|
||||
private List<DisplayObject> display;
|
||||
|
||||
@JsonProperty(VERIFIABLE_CREDENTIAL_TYPE_KEY)
|
||||
private String vct;
|
||||
|
||||
@ -84,8 +79,8 @@ public class SupportedCredentialConfiguration {
|
||||
@JsonProperty(PROOF_TYPES_SUPPORTED_KEY)
|
||||
private ProofTypesSupported proofTypesSupported;
|
||||
|
||||
@JsonProperty(CLAIMS_KEY)
|
||||
private Claims claims;
|
||||
@JsonProperty(CREDENTIAL_METADATA_KEY)
|
||||
private CredentialMetadata credentialMetadata;
|
||||
|
||||
// This is not a normative field for supported credential metadata,
|
||||
// but will allow configuring the issuance of the credential internally.
|
||||
@ -133,16 +128,15 @@ public class SupportedCredentialConfiguration {
|
||||
String bindingMethodsSupported = CredentialScopeModel.CRYPTOGRAPHIC_BINDING_METHODS_DEFAULT;
|
||||
credentialConfiguration.setCryptographicBindingMethodsSupported(List.of(bindingMethodsSupported));
|
||||
|
||||
credentialConfiguration.setDisplay(DisplayObject.parse(credentialScope));
|
||||
// Parse credential metadata (includes display and claims)
|
||||
CredentialMetadata credentialMetadata = CredentialMetadata.parse(keycloakSession, credentialScope);
|
||||
credentialConfiguration.setCredentialMetadata(credentialMetadata);
|
||||
|
||||
CredentialBuildConfig credentialBuildConfig = CredentialBuildConfig.parse(keycloakSession,
|
||||
credentialConfiguration,
|
||||
credentialScope);
|
||||
credentialConfiguration.setCredentialBuildConfig(credentialBuildConfig);
|
||||
|
||||
Claims claims = Claims.parse(keycloakSession, credentialScope);
|
||||
credentialConfiguration.setClaims(claims);
|
||||
|
||||
return credentialConfiguration;
|
||||
}
|
||||
|
||||
@ -196,15 +190,6 @@ public class SupportedCredentialConfiguration {
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<DisplayObject> getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setDisplay(List<DisplayObject> display) {
|
||||
this.display = display;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
@ -223,15 +208,6 @@ public class SupportedCredentialConfiguration {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Claims getClaims() {
|
||||
return claims;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setClaims(Claims claims) {
|
||||
this.claims = claims;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getVct() {
|
||||
return vct;
|
||||
}
|
||||
@ -259,6 +235,15 @@ public class SupportedCredentialConfiguration {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialMetadata getCredentialMetadata() {
|
||||
return credentialMetadata;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setCredentialMetadata(CredentialMetadata credentialMetadata) {
|
||||
this.credentialMetadata = credentialMetadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialBuildConfig getCredentialBuildConfig() {
|
||||
return credentialBuildConfig;
|
||||
}
|
||||
@ -273,11 +258,11 @@ public class SupportedCredentialConfiguration {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SupportedCredentialConfiguration that = (SupportedCredentialConfiguration) o;
|
||||
return Objects.equals(id, that.id) && Objects.equals(format, that.format) && Objects.equals(scope, that.scope) && Objects.equals(cryptographicBindingMethodsSupported, that.cryptographicBindingMethodsSupported) && Objects.equals(credentialSigningAlgValuesSupported, that.credentialSigningAlgValuesSupported) && Objects.equals(display, that.display) && Objects.equals(vct, that.vct) && Objects.equals(credentialDefinition, that.credentialDefinition) && Objects.equals(proofTypesSupported, that.proofTypesSupported) && Objects.equals(claims, that.claims) && Objects.equals(credentialBuildConfig, that.credentialBuildConfig);
|
||||
return Objects.equals(id, that.id) && Objects.equals(format, that.format) && Objects.equals(scope, that.scope) && Objects.equals(cryptographicBindingMethodsSupported, that.cryptographicBindingMethodsSupported) && Objects.equals(credentialSigningAlgValuesSupported, that.credentialSigningAlgValuesSupported) && Objects.equals(vct, that.vct) && Objects.equals(credentialDefinition, that.credentialDefinition) && Objects.equals(proofTypesSupported, that.proofTypesSupported) && Objects.equals(credentialMetadata, that.credentialMetadata) && Objects.equals(credentialBuildConfig, that.credentialBuildConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, format, scope, cryptographicBindingMethodsSupported, credentialSigningAlgValuesSupported, display, vct, credentialDefinition, proofTypesSupported, claims, credentialBuildConfig);
|
||||
return Objects.hash(id, format, scope, cryptographicBindingMethodsSupported, credentialSigningAlgValuesSupported, vct, credentialDefinition, proofTypesSupported, credentialMetadata, credentialBuildConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ import org.keycloak.protocol.oid4vc.model.ClaimDisplay;
|
||||
import org.keycloak.protocol.oid4vc.model.Claims;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequestEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
|
||||
@ -126,15 +127,21 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
Assert.assertNotNull("credential_response_encryption should be present", encryption);
|
||||
Assert.assertEquals(List.of(RSA_OAEP, RSA_OAEP_256), encryption.getAlgValuesSupported());
|
||||
Assert.assertEquals(List.of(A256GCM), encryption.getEncValuesSupported());
|
||||
Assert.assertNotNull("zip_values_supported should be present", encryption.getZipValuesSupported());
|
||||
Assert.assertTrue("encryption_required should be true", encryption.getEncryptionRequired());
|
||||
|
||||
// Check credential_request_encryption
|
||||
CredentialRequestEncryptionMetadata requestEncryption = credentialIssuer.getCredentialRequestEncryption();
|
||||
Assert.assertNotNull("credential_request_encryption should be present", requestEncryption);
|
||||
Assert.assertEquals(List.of(A256GCM), requestEncryption.getEncValuesSupported());
|
||||
Assert.assertNotNull("zip_values_supported should be present", requestEncryption.getZipValuesSupported());
|
||||
Assert.assertTrue("encryption_required should be true", requestEncryption.getEncryptionRequired());
|
||||
Assert.assertNotNull("JWKS should be present", requestEncryption.getJwks());
|
||||
Assert.assertFalse("JWKS should not be empty when encryption keys are available", requestEncryption.getJwks().isEmpty());
|
||||
|
||||
CredentialIssuer.BatchCredentialIssuance batch = credentialIssuer.getBatchCredentialIssuance();
|
||||
Assert.assertNotNull("batch_credential_issuance should be present", batch);
|
||||
Assert.assertEquals(Integer.valueOf(10), batch.getBatchSize());
|
||||
Assert.assertEquals(
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc",
|
||||
credentialIssuer.getSignedMetadata()
|
||||
);
|
||||
|
||||
for (ClientScopeRepresentation clientScope : List.of(jwtTypeCredentialClientScope,
|
||||
sdJwtTypeCredentialClientScope,
|
||||
@ -159,10 +166,10 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
Assert.assertEquals(clientScope.getName(), supportedConfig.getCredentialDefinition().getType().get(0));
|
||||
Assert.assertEquals(1, supportedConfig.getCredentialDefinition().getContext().size());
|
||||
Assert.assertEquals(clientScope.getName(), supportedConfig.getCredentialDefinition().getContext().get(0));
|
||||
Assert.assertNull(supportedConfig.getDisplay());
|
||||
Assert.assertNotNull(supportedConfig.getCredentialMetadata());
|
||||
Assert.assertEquals(clientScope.getName(), supportedConfig.getScope());
|
||||
|
||||
compareClaims(supportedConfig.getFormat(), supportedConfig.getClaims(), clientScope.getProtocolMappers());
|
||||
compareClaims(supportedConfig.getFormat(), supportedConfig.getCredentialMetadata().getClaims(), clientScope.getProtocolMappers());
|
||||
}
|
||||
|
||||
|
||||
@ -180,10 +187,16 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
|
||||
Assert.assertTrue(encryption.getAlgValuesSupported().contains(RSA_OAEP));
|
||||
Assert.assertTrue("Supported encryption methods should include A256GCM", encryption.getEncValuesSupported().contains(A256GCM));
|
||||
Assert.assertNotNull("zip_values_supported should be present", encryption.getZipValuesSupported());
|
||||
Assert.assertTrue(encryption.getEncryptionRequired());
|
||||
|
||||
// Check credential_request_encryption
|
||||
CredentialRequestEncryptionMetadata requestEncryption = issuer.getCredentialRequestEncryption();
|
||||
Assert.assertNotNull("credential_request_encryption should be present", requestEncryption);
|
||||
Assert.assertTrue("Supported encryption methods should include A256GCM", requestEncryption.getEncValuesSupported().contains(A256GCM));
|
||||
Assert.assertNotNull("zip_values_supported should be present", requestEncryption.getZipValuesSupported());
|
||||
Assert.assertTrue("encryption_required should be true", requestEncryption.getEncryptionRequired());
|
||||
Assert.assertEquals(Integer.valueOf(10), issuer.getBatchCredentialIssuance().getBatchSize());
|
||||
Assert.assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc",
|
||||
issuer.getSignedMetadata());
|
||||
});
|
||||
}
|
||||
|
||||
@ -216,10 +229,24 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().isEmpty());
|
||||
Assert.assertFalse("Supported encryption methods should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().isEmpty());
|
||||
Assert.assertNotNull("zip_values_supported should be present",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getZipValuesSupported());
|
||||
Assert.assertTrue("Supported algorithms should include RSA-OAEP",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().contains("RSA-OAEP"));
|
||||
Assert.assertTrue("Supported encryption methods should include A256GCM",
|
||||
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().contains("A256GCM"));
|
||||
Assert.assertNotNull("Credential request encryption should be advertised in metadata",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption());
|
||||
Assert.assertFalse("Supported encryption methods should not be empty",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getEncValuesSupported().isEmpty());
|
||||
Assert.assertNotNull("zip_values_supported should be present",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getZipValuesSupported());
|
||||
Assert.assertTrue("Supported encryption methods should include A256GCM",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getEncValuesSupported().contains("A256GCM"));
|
||||
Assert.assertNotNull("JWKS should be present in credential request encryption",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getJwks());
|
||||
Assert.assertFalse("JWKS should not be empty when encryption keys are available",
|
||||
oid4vciIssuerConfig.getCredentialRequestEncryption().getJwks().isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -292,13 +319,14 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
compareClaims(expectedFormat, supportedConfig.getClaims(), clientScope.getProtocolMappers());
|
||||
compareClaims(expectedFormat, supportedConfig.getCredentialMetadata().getClaims(), clientScope.getProtocolMappers());
|
||||
|
||||
}
|
||||
|
||||
private void compareDisplay(SupportedCredentialConfiguration supportedConfig, ClientScopeRepresentation clientScope) {
|
||||
String display = clientScope.getAttributes().get(CredentialScopeModel.VC_DISPLAY);
|
||||
if (StringUtil.isBlank(display)) {
|
||||
Assert.assertNull(supportedConfig.getDisplay());
|
||||
Assert.assertNull(supportedConfig.getCredentialMetadata() != null ? supportedConfig.getCredentialMetadata().getDisplay() : null);
|
||||
return;
|
||||
}
|
||||
List<DisplayObject> expectedDisplayObjectList;
|
||||
@ -309,9 +337,10 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Assert.assertEquals(expectedDisplayObjectList.size(), supportedConfig.getDisplay().size());
|
||||
Assert.assertNotNull("Credential metadata should exist when display is configured", supportedConfig.getCredentialMetadata());
|
||||
Assert.assertEquals(expectedDisplayObjectList.size(), supportedConfig.getCredentialMetadata().getDisplay().size());
|
||||
MatcherAssert.assertThat("Must contain all expected display-objects",
|
||||
supportedConfig.getDisplay(),
|
||||
supportedConfig.getCredentialMetadata().getDisplay(),
|
||||
Matchers.containsInAnyOrder(expectedDisplayObjectList.toArray()));
|
||||
}
|
||||
|
||||
@ -410,7 +439,9 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
Assert.assertTrue("The test-credential should be supported.", credentialIssuer.getCredentialsSupported().containsKey("test-credential"));
|
||||
Assert.assertEquals("The test-credential should offer type VerifiableCredential", "VerifiableCredential", credentialIssuer.getCredentialsSupported().get("test-credential").getScope());
|
||||
Assert.assertEquals("The test-credential should be offered in the jwt-vc format.", Format.JWT_VC, credentialIssuer.getCredentialsSupported().get("test-credential").getFormat());
|
||||
Assert.assertNotNull("The test-credential can optionally provide a claims claim.", credentialIssuer.getCredentialsSupported().get("test-credential").getClaims());
|
||||
Assert.assertNotNull("The test-credential can optionally provide a claims claim.",
|
||||
credentialIssuer.getCredentialsSupported().get("test-credential").getCredentialMetadata() != null ?
|
||||
credentialIssuer.getCredentialsSupported().get("test-credential").getCredentialMetadata().getClaims() : null);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -936,7 +936,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
Format.JWT_VC,
|
||||
jwtVcConfig.getFormat());
|
||||
|
||||
Claims jwtVcClaims = jwtVcConfig.getClaims();
|
||||
Claims jwtVcClaims = jwtVcConfig.getCredentialMetadata() != null ? jwtVcConfig.getCredentialMetadata().getClaims() : null;
|
||||
assertNotNull("The jwt_vc-credential can optionally provide a claims claim.",
|
||||
jwtVcClaims);
|
||||
|
||||
@ -1047,7 +1047,8 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
.contains("RS256"));
|
||||
assertEquals("The jwt_vc-credential should display as Test Credential",
|
||||
credentialConfigurationId,
|
||||
jwtVcConfig.getDisplay().get(0).getName());
|
||||
jwtVcConfig.getCredentialMetadata() != null && jwtVcConfig.getCredentialMetadata().getDisplay() != null ?
|
||||
jwtVcConfig.getCredentialMetadata().getDisplay().get(0).getName() : null);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -395,9 +395,11 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
|
||||
assertNotNull("The sd-jwt-credential can optionally provide a claims claim.",
|
||||
credentialIssuer.getCredentialsSupported().get(credentialConfigurationId)
|
||||
.getClaims());
|
||||
.getCredentialMetadata() != null ?
|
||||
credentialIssuer.getCredentialsSupported().get(credentialConfigurationId)
|
||||
.getCredentialMetadata().getClaims() : null);
|
||||
|
||||
Claims jwtVcClaims = jwtVcConfig.getClaims();
|
||||
Claims jwtVcClaims = jwtVcConfig.getCredentialMetadata() != null ? jwtVcConfig.getCredentialMetadata().getClaims() : null;
|
||||
assertNotNull("The sd-jwt-credential can optionally provide a claims claim.",
|
||||
jwtVcClaims);
|
||||
|
||||
@ -476,7 +478,11 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
|
||||
assertEquals("The sd-jwt-credential should display as Test Credential",
|
||||
credentialConfigurationId,
|
||||
credentialIssuer.getCredentialsSupported().get(credentialConfigurationId)
|
||||
.getDisplay().get(0).getName());
|
||||
.getCredentialMetadata() != null &&
|
||||
credentialIssuer.getCredentialsSupported().get(credentialConfigurationId)
|
||||
.getCredentialMetadata().getDisplay() != null ?
|
||||
credentialIssuer.getCredentialsSupported().get(credentialConfigurationId)
|
||||
.getCredentialMetadata().getDisplay().get(0).getName() : null);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user