Enhance SupportedCredentialConfiguration to support optional claims object as defined in OpenID for Verifiable Credential Issuance specification (#30420)

closes #30419 

Signed-off-by: Francis Pouatcha <francis.pouatcha@adorsys.com>
This commit is contained in:
Francis Pouatcha 2024-06-18 16:07:49 +01:00 committed by GitHub
parent fc65c73106
commit d4797e04a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 618 additions and 79 deletions

View file

@ -0,0 +1,66 @@
/*
* 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.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* Holding metadata on a claim of verifiable credential.
* <p>
* See: <a href="https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-A.2.2">openid-4-verifiable-credential-issuance-1_0.html#appendix-A.2.2</a>
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Claim {
@JsonProperty("mandatory")
private Boolean mandatory;
@JsonProperty("value_type")
private String valueType;
@JsonProperty("display")
private List<ClaimDisplay> display;
public Boolean getMandatory() {
return mandatory;
}
public Claim setMandatory(Boolean mandatory) {
this.mandatory = mandatory;
return this;
}
public String getValueType() {
return valueType;
}
public Claim setValueType(String valueType) {
this.valueType = valueType;
return this;
}
public List<ClaimDisplay> getDisplay() {
return display;
}
public Claim setDisplay(List<ClaimDisplay> display) {
this.display = display;
return this;
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ClaimDisplay {
@JsonProperty("name")
private String name;
@JsonProperty("locale")
private String locale;
public String getName() {
return name;
}
public ClaimDisplay setName(String name) {
this.name = name;
return this;
}
public String getLocale() {
return locale;
}
public ClaimDisplay setLocale(String locale) {
this.locale = locale;
return this;
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.model;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.HashMap;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class Claims extends HashMap<String, Claim> {
public String toJsonString(){
try {
return JsonSerialization.writeValueAsString(this);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Claims fromJsonString(String jsonString){
try {
return JsonSerialization.readValue(jsonString, Claims.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -14,17 +14,15 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.protocol.oid4vc.model; package org.keycloak.protocol.oid4vc.model;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.util.JsonSerialization;
import java.util.HashMap; import java.io.IOException;
import java.util.Map;
import java.util.Optional;
/** /**
* Represents a DisplayObject, as used in the OID4VCI Credentials Issuer Metadata * Represents a DisplayObject, as used in the OID4VCI Credentials Issuer Metadata
@ -126,26 +124,20 @@ public class DisplayObject {
return this; return this;
} }
public Map<String, String> toDotNotation() { public String toJsonString(){
Map<String, String> dotNotation = new HashMap<>(); try {
dotNotation.put(NAME_KEY, name); return JsonSerialization.writeValueAsString(this);
dotNotation.put(LOCALE_KEY, locale); } catch (IOException e) {
dotNotation.put(LOGO_KEY, logo); throw new RuntimeException(e);
dotNotation.put(DESCRIPTION_KEY, description); }
dotNotation.put(BG_COLOR_KEY, backgroundColor);
dotNotation.put(TEXT_COLOR_KEY, textColor);
return dotNotation;
} }
public static DisplayObject fromDotNotation(Map<String, String> dotNotated) { public static DisplayObject fromJsonString(String jsonString){
DisplayObject displayObject = new DisplayObject(); try {
Optional.ofNullable(dotNotated.get(NAME_KEY)).ifPresent(displayObject::setName); return JsonSerialization.readValue(jsonString, DisplayObject.class);
Optional.ofNullable(dotNotated.get(LOCALE_KEY)).ifPresent(displayObject::setLocale); } catch (IOException e) {
Optional.ofNullable(dotNotated.get(LOGO_KEY)).ifPresent(displayObject::setLogo); throw new RuntimeException(e);
Optional.ofNullable(dotNotated.get(DESCRIPTION_KEY)).ifPresent(displayObject::setDescription); }
Optional.ofNullable(dotNotated.get(BG_COLOR_KEY)).ifPresent(displayObject::setBackgroundColor);
Optional.ofNullable(dotNotated.get(TEXT_COLOR_KEY)).ifPresent(displayObject::setTextColor);
return displayObject;
} }
@Override @Override
@ -173,4 +165,4 @@ public class DisplayObject {
result = 31 * result + (getTextColor() != null ? getTextColor().hashCode() : 0); result = 31 * result + (getTextColor() != null ? getTextColor().hashCode() : 0);
return result; return result;
} }
} }

View file

@ -0,0 +1,81 @@
/*
* 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.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Objects;
/**
* See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-cwt-proof-type
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProofTypeCWT {
@JsonProperty("proof_signing_alg_values_supported")
private List<Integer> proofSigningAlgValuesSupported;
@JsonProperty("proof_alg_values_supported")
private List<Integer> proofAlgValuesSupported;
@JsonProperty("proof_crv_values_supported")
private List<Integer> proofCrvValuesSupported;
public List<Integer> getProofSigningAlgValuesSupported() {
return proofSigningAlgValuesSupported;
}
public ProofTypeCWT setProofSigningAlgValuesSupported(List<Integer> proofSigningAlgValuesSupported) {
this.proofSigningAlgValuesSupported = proofSigningAlgValuesSupported;
return this;
}
public List<Integer> getProofAlgValuesSupported() {
return proofAlgValuesSupported;
}
public ProofTypeCWT setProofAlgValuesSupported(List<Integer> proofAlgValuesSupported) {
this.proofAlgValuesSupported = proofAlgValuesSupported;
return this;
}
public List<Integer> getProofCrvValuesSupported() {
return proofCrvValuesSupported;
}
public ProofTypeCWT setProofCrvValuesSupported(List<Integer> proofCrvValuesSupported) {
this.proofCrvValuesSupported = proofCrvValuesSupported;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProofTypeCWT that = (ProofTypeCWT) o;
return Objects.equals(proofSigningAlgValuesSupported, that.proofSigningAlgValuesSupported) && Objects.equals(proofAlgValuesSupported, that.proofAlgValuesSupported) && Objects.equals(proofCrvValuesSupported, that.proofCrvValuesSupported);
}
@Override
public int hashCode() {
return Objects.hash(proofSigningAlgValuesSupported, proofAlgValuesSupported, proofCrvValuesSupported);
}
}

View file

@ -0,0 +1,56 @@
/*
* 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.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Objects;
/**
* See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-jwt-proof-type
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProofTypeJWT {
@JsonProperty("proof_signing_alg_values_supported")
private List<String> proofSigningAlgValuesSupported;
public List<String> getProofSigningAlgValuesSupported() {
return proofSigningAlgValuesSupported;
}
public ProofTypeJWT setProofSigningAlgValuesSupported(List<String> proofSigningAlgValuesSupported) {
this.proofSigningAlgValuesSupported = proofSigningAlgValuesSupported;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProofTypeJWT that = (ProofTypeJWT) o;
return Objects.equals(proofSigningAlgValuesSupported, that.proofSigningAlgValuesSupported);
}
@Override
public int hashCode() {
return Objects.hash(proofSigningAlgValuesSupported);
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.model;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-ldp_vp-proof-type
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProofTypeLdpVp {
}

View file

@ -0,0 +1,96 @@
/*
* 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.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.Objects;
/**
* See: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-proof-types
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProofTypesSupported {
@JsonProperty("jwt")
private ProofTypeJWT jwt;
@JsonProperty("cwt")
private ProofTypeCWT cwt;
@JsonProperty("ldp_vp")
private ProofTypeLdpVp ldpVp;
public ProofTypeJWT getJwt() {
return jwt;
}
public ProofTypesSupported setJwt(ProofTypeJWT jwt) {
this.jwt = jwt;
return this;
}
public ProofTypeCWT getCwt() {
return cwt;
}
public ProofTypesSupported setCwt(ProofTypeCWT cwt) {
this.cwt = cwt;
return this;
}
public ProofTypeLdpVp getLdpVp() {
return ldpVp;
}
public ProofTypesSupported setLdpVp(ProofTypeLdpVp ldpVp) {
this.ldpVp = ldpVp;
return this;
}
public String toJsonString(){
try {
return JsonSerialization.writeValueAsString(this);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static ProofTypesSupported fromJsonString(String jsonString){
try {
return JsonSerialization.readValue(jsonString, ProofTypesSupported.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProofTypesSupported that = (ProofTypesSupported) o;
return Objects.equals(jwt, that.jwt) && Objects.equals(cwt, that.cwt) && Objects.equals(ldpVp, that.ldpVp);
}
@Override
public int hashCode() {
return Objects.hash(jwt, cwt, ldpVp);
}
}

View file

@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.protocol.oid4vc.model; package org.keycloak.protocol.oid4vc.model;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@ -26,7 +25,9 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
/** /**
* A supported credential, as used in the Credentials Issuer Metadata in OID4VCI * A supported credential, as used in the Credentials Issuer Metadata in OID4VCI
@ -42,13 +43,19 @@ public class SupportedCredentialConfiguration {
@JsonIgnore @JsonIgnore
private static final String SCOPE_KEY = "scope"; private static final String SCOPE_KEY = "scope";
@JsonIgnore @JsonIgnore
private static final String CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY = " credential_signing_alg_values_supported"; private static final String CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY = "cryptographic_binding_methods_supported";
@JsonIgnore @JsonIgnore
private static final String CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY = "cryptographic_suites_supported"; private static final String CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY = "cryptographic_suites_supported";
@JsonIgnore @JsonIgnore
private static final String CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY = "credential_signing_alg_values_supported"; private static final String CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY = "credential_signing_alg_values_supported";
@JsonIgnore @JsonIgnore
private static final String DISPLAY_KEY = "display"; 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
private static final String VERIFIABLE_CREDENTIAL_TYPE_KEY = "vct";
private String id; private String id;
@JsonProperty(FORMAT_KEY) @JsonProperty(FORMAT_KEY)
@ -67,7 +74,16 @@ public class SupportedCredentialConfiguration {
private List<String> credentialSigningAlgValuesSupported; private List<String> credentialSigningAlgValuesSupported;
@JsonProperty(DISPLAY_KEY) @JsonProperty(DISPLAY_KEY)
private DisplayObject display; private List<DisplayObject> display;
@JsonProperty(VERIFIABLE_CREDENTIAL_TYPE_KEY)
private String vct;
@JsonProperty(PROOF_TYPES_SUPPORTED_KEY)
private ProofTypesSupported proofTypesSupported;
@JsonProperty(CLAIMS_KEY)
private Claims claims;
public Format getFormat() { public Format getFormat() {
return format; return format;
@ -105,11 +121,11 @@ public class SupportedCredentialConfiguration {
return this; return this;
} }
public DisplayObject getDisplay() { public List<DisplayObject> getDisplay() {
return display; return display;
} }
public SupportedCredentialConfiguration setDisplay(DisplayObject display) { public SupportedCredentialConfiguration setDisplay(List<DisplayObject> display) {
this.display = display; this.display = display;
return this; return this;
} }
@ -135,9 +151,37 @@ public class SupportedCredentialConfiguration {
return this; return this;
} }
public Claims getClaims() {
return claims;
}
public SupportedCredentialConfiguration setClaims(Claims claims) {
this.claims = claims;
return this;
}
public String getVct() {
return vct;
}
public SupportedCredentialConfiguration setVct(String vct) {
this.vct = vct;
return this;
}
public ProofTypesSupported getProofTypesSupported() {
return proofTypesSupported;
}
public SupportedCredentialConfiguration setProofTypesSupported(ProofTypesSupported proofTypesSupported) {
this.proofTypesSupported = proofTypesSupported;
return this;
}
public Map<String, String> toDotNotation() { public Map<String, String> toDotNotation() {
Map<String, String> dotNotation = new HashMap<>(); Map<String, String> dotNotation = new HashMap<>();
Optional.ofNullable(format).ifPresent(format -> dotNotation.put(id + DOT_SEPARATOR + FORMAT_KEY, format.toString())); Optional.ofNullable(format).ifPresent(format -> dotNotation.put(id + DOT_SEPARATOR + FORMAT_KEY, format.toString()));
Optional.ofNullable(vct).ifPresent(vct -> dotNotation.put(id + DOT_SEPARATOR + VERIFIABLE_CREDENTIAL_TYPE_KEY, vct));
Optional.ofNullable(scope).ifPresent(scope -> dotNotation.put(id + DOT_SEPARATOR + SCOPE_KEY, scope)); Optional.ofNullable(scope).ifPresent(scope -> dotNotation.put(id + DOT_SEPARATOR + SCOPE_KEY, scope));
Optional.ofNullable(cryptographicBindingMethodsSupported).ifPresent(types -> Optional.ofNullable(cryptographicBindingMethodsSupported).ifPresent(types ->
dotNotation.put(id + DOT_SEPARATOR + CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY, String.join(",", cryptographicBindingMethodsSupported))); dotNotation.put(id + DOT_SEPARATOR + CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY, String.join(",", cryptographicBindingMethodsSupported)));
@ -145,13 +189,16 @@ public class SupportedCredentialConfiguration {
dotNotation.put(id + DOT_SEPARATOR + CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY, String.join(",", cryptographicSuitesSupported))); dotNotation.put(id + DOT_SEPARATOR + CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY, String.join(",", cryptographicSuitesSupported)));
Optional.ofNullable(cryptographicSuitesSupported).ifPresent(types -> Optional.ofNullable(cryptographicSuitesSupported).ifPresent(types ->
dotNotation.put(id + DOT_SEPARATOR + CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY, String.join(",", credentialSigningAlgValuesSupported))); dotNotation.put(id + DOT_SEPARATOR + CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY, String.join(",", credentialSigningAlgValuesSupported)));
Optional.ofNullable(claims).ifPresent(c -> dotNotation.put(id + DOT_SEPARATOR + CLAIMS_KEY, c.toJsonString()));
Optional.ofNullable(display)
.ifPresent(d -> d.stream()
.filter(Objects::nonNull)
.forEach(o -> dotNotation.put(id + DOT_SEPARATOR + DISPLAY_KEY + DOT_SEPARATOR + d.indexOf(o), o.toJsonString())));
Optional.ofNullable(proofTypesSupported)
.ifPresent(p -> dotNotation.put(id + DOT_SEPARATOR + PROOF_TYPES_SUPPORTED_KEY, p.toJsonString()));
Map<String, String> dotNotatedDisplay = Optional.ofNullable(display)
.map(DisplayObject::toDotNotation)
.orElse(Map.of());
dotNotatedDisplay.entrySet().stream()
.filter(entry -> entry.getValue() != null)
.forEach(entry -> dotNotation.put(id + DOT_SEPARATOR + DISPLAY_KEY + "." + entry.getKey(), entry.getValue()));
return dotNotation; return dotNotation;
} }
@ -159,6 +206,7 @@ public class SupportedCredentialConfiguration {
SupportedCredentialConfiguration supportedCredentialConfiguration = new SupportedCredentialConfiguration().setId(credentialId); SupportedCredentialConfiguration supportedCredentialConfiguration = new SupportedCredentialConfiguration().setId(credentialId);
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + FORMAT_KEY)).map(Format::fromString).ifPresent(supportedCredentialConfiguration::setFormat); Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + FORMAT_KEY)).map(Format::fromString).ifPresent(supportedCredentialConfiguration::setFormat);
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + VERIFIABLE_CREDENTIAL_TYPE_KEY)).ifPresent(supportedCredentialConfiguration::setVct);
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + SCOPE_KEY)).ifPresent(supportedCredentialConfiguration::setScope); Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + SCOPE_KEY)).ifPresent(supportedCredentialConfiguration::setScope);
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY)) Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY))
.map(cbms -> cbms.split(",")) .map(cbms -> cbms.split(","))
@ -172,45 +220,38 @@ public class SupportedCredentialConfiguration {
.map(css -> css.split(",")) .map(css -> css.split(","))
.map(Arrays::asList) .map(Arrays::asList)
.ifPresent(supportedCredentialConfiguration::setCredentialSigningAlgValuesSupported); .ifPresent(supportedCredentialConfiguration::setCredentialSigningAlgValuesSupported);
Map<String, String> displayMap = new HashMap<>(); Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + CLAIMS_KEY))
dotNotated.entrySet().forEach(entry -> { .map(Claims::fromJsonString)
String key = entry.getKey(); .ifPresent(supportedCredentialConfiguration::setClaims);
if (key.startsWith(credentialId + DOT_SEPARATOR + DISPLAY_KEY)) {
displayMap.put(key.substring((credentialId + DOT_SEPARATOR + DISPLAY_KEY).length() + 1), entry.getValue()); String displayKeyPrefix = credentialId + DOT_SEPARATOR + DISPLAY_KEY + DOT_SEPARATOR;
} List<DisplayObject> displayList = dotNotated.entrySet().stream()
}); .filter(entry -> entry.getKey().startsWith(displayKeyPrefix))
if (!displayMap.isEmpty()) { .sorted(Map.Entry.comparingByKey())
supportedCredentialConfiguration.setDisplay(DisplayObject.fromDotNotation(displayMap)); .map(entry -> DisplayObject.fromJsonString(entry.getValue()))
.collect(Collectors.toList());
if(!displayList.isEmpty()){
supportedCredentialConfiguration.setDisplay(displayList);
} }
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + PROOF_TYPES_SUPPORTED_KEY))
.map(ProofTypesSupported::fromJsonString)
.ifPresent(supportedCredentialConfiguration::setProofTypesSupported);
return supportedCredentialConfiguration; return supportedCredentialConfiguration;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (!(o instanceof SupportedCredentialConfiguration that)) return false; if (o == null || getClass() != o.getClass()) return false;
SupportedCredentialConfiguration that = (SupportedCredentialConfiguration) o;
if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false; return Objects.equals(id, that.id) && format == that.format && Objects.equals(scope, that.scope) && Objects.equals(cryptographicBindingMethodsSupported, that.cryptographicBindingMethodsSupported) && Objects.equals(cryptographicSuitesSupported, that.cryptographicSuitesSupported) && Objects.equals(credentialSigningAlgValuesSupported, that.credentialSigningAlgValuesSupported) && Objects.equals(display, that.display) && Objects.equals(vct, that.vct) && Objects.equals(proofTypesSupported, that.proofTypesSupported) && Objects.equals(claims, that.claims);
if (getFormat() != that.getFormat()) return false;
if (getScope() != null ? !getScope().equals(that.getScope()) : that.getScope() != null) return false;
if (getCryptographicBindingMethodsSupported() != null ? !getCryptographicBindingMethodsSupported().equals(that.getCryptographicBindingMethodsSupported()) : that.getCryptographicBindingMethodsSupported() != null)
return false;
if (getCryptographicSuitesSupported() != null ? !getCryptographicSuitesSupported().equals(that.getCryptographicSuitesSupported()) : that.getCryptographicSuitesSupported() != null)
return false;
if (getCredentialSigningAlgValuesSupported() != null ? !getCredentialSigningAlgValuesSupported().equals(that.getCredentialSigningAlgValuesSupported()) : that.getCredentialSigningAlgValuesSupported() != null)
return false;
return getDisplay() != null ? getDisplay().equals(that.getDisplay()) : that.getDisplay() == null;
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = getId() != null ? getId().hashCode() : 0; return Objects.hash(id, format, scope, cryptographicBindingMethodsSupported, cryptographicSuitesSupported, credentialSigningAlgValuesSupported, display, vct, proofTypesSupported, claims);
result = 31 * result + (getFormat() != null ? getFormat().hashCode() : 0);
result = 31 * result + (getScope() != null ? getScope().hashCode() : 0);
result = 31 * result + (getCryptographicBindingMethodsSupported() != null ? getCryptographicBindingMethodsSupported().hashCode() : 0);
result = 31 * result + (getCryptographicSuitesSupported() != null ? getCryptographicSuitesSupported().hashCode() : 0);
result = 31 * result + (getCredentialSigningAlgValuesSupported() != null ? getCredentialSigningAlgValuesSupported().hashCode() : 0);
result = 31 * result + (getDisplay() != null ? getDisplay().hashCode() : 0);
return result;
} }
} }

View file

@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.protocol.oid4vc; package org.keycloak.protocol.oid4vc;
import org.junit.Test; import org.junit.Test;
@ -23,6 +22,8 @@ import org.junit.runners.Parameterized;
import org.keycloak.protocol.oid4vc.model.DisplayObject; import org.keycloak.protocol.oid4vc.model.DisplayObject;
import org.keycloak.protocol.oid4vc.model.Format; import org.keycloak.protocol.oid4vc.model.Format;
import org.keycloak.protocol.oid4vc.model.OID4VCClient; import org.keycloak.protocol.oid4vc.model.OID4VCClient;
import org.keycloak.protocol.oid4vc.model.ProofTypeJWT;
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration; import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
import java.util.Arrays; import java.util.Arrays;
@ -31,7 +32,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public class OID4VCClientRegistrationProviderTest { public class OID4VCClientRegistrationProviderTest {
@ -68,13 +68,12 @@ public class OID4VCClientRegistrationProviderTest {
Map.of( Map.of(
"vc.credential-id.format", Format.JWT_VC.toString(), "vc.credential-id.format", Format.JWT_VC.toString(),
"vc.credential-id.scope", "AnotherCredential", "vc.credential-id.scope", "AnotherCredential",
"vc.credential-id.display.name", "Another", "vc.credential-id.display.0", "{\"name\":\"Another\",\"locale\":\"en\"}"),
"vc.credential-id.display.locale", "en"),
new OID4VCClient(null, "did:web:test.org", new OID4VCClient(null, "did:web:test.org",
List.of(new SupportedCredentialConfiguration() List.of(new SupportedCredentialConfiguration()
.setId("credential-id") .setId("credential-id")
.setFormat(Format.JWT_VC) .setFormat(Format.JWT_VC)
.setDisplay(new DisplayObject().setLocale("en").setName("Another")) .setDisplay(Arrays.asList(new DisplayObject().setLocale("en").setName("Another")))
.setScope("AnotherCredential")), .setScope("AnotherCredential")),
null, null) null, null)
}, },
@ -83,23 +82,23 @@ public class OID4VCClientRegistrationProviderTest {
Map.of( Map.of(
"vc.first-id.format", Format.JWT_VC.toString(), "vc.first-id.format", Format.JWT_VC.toString(),
"vc.first-id.scope", "AnotherCredential", "vc.first-id.scope", "AnotherCredential",
"vc.first-id.display.name", "First", "vc.first-id.display.0", "{\"name\":\"First\",\"locale\":\"en\"}",
"vc.first-id.display.locale", "en",
"vc.second-id.format", Format.SD_JWT_VC.toString(), "vc.second-id.format", Format.SD_JWT_VC.toString(),
"vc.second-id.scope", "MyType", "vc.second-id.scope", "MyType",
"vc.second-id.display.name", "Second Credential", "vc.second-id.display.0", "{\"name\":\"Second Credential\",\"locale\":\"de\"}",
"vc.second-id.display.locale", "de"), "vc.second-id.proof_types_supported","{\"jwt\":{\"proof_signing_alg_values_supported\":[\"ES256\"]}}"),
new OID4VCClient(null, "did:web:test.org", new OID4VCClient(null, "did:web:test.org",
List.of(new SupportedCredentialConfiguration() List.of(new SupportedCredentialConfiguration()
.setId("first-id") .setId("first-id")
.setFormat(Format.JWT_VC) .setFormat(Format.JWT_VC)
.setDisplay(new DisplayObject().setLocale("en").setName("First")) .setDisplay(Arrays.asList(new DisplayObject().setLocale("en").setName("First")))
.setScope("AnotherCredential"), .setScope("AnotherCredential"),
new SupportedCredentialConfiguration() new SupportedCredentialConfiguration()
.setId("second-id") .setId("second-id")
.setFormat(Format.SD_JWT_VC) .setFormat(Format.SD_JWT_VC)
.setDisplay(new DisplayObject().setLocale("de").setName("Second Credential")) .setDisplay(Arrays.asList(new DisplayObject().setLocale("de").setName("Second Credential")))
.setScope("MyType")), .setScope("MyType")
.setProofTypesSupported(new ProofTypesSupported().setJwt(new ProofTypeJWT().setProofSigningAlgValuesSupported(Arrays.asList("ES256"))))),
null, null) null, null)
} }
}); });
@ -129,4 +128,4 @@ public class OID4VCClientRegistrationProviderTest {
OID4VCClientRegistrationProvider.fromClientAttributes("did:web:test.org", clientAttributes)); OID4VCClientRegistrationProvider.fromClientAttributes("did:web:test.org", clientAttributes));
} }
} }

View file

@ -0,0 +1,66 @@
/*
* 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.model;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;
import org.keycloak.util.JsonSerialization;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class ClaimsTest {
@Test
public void toJsonString() throws JsonProcessingException {
Claims claims = new Claims();
claims.put("firstName", new Claim());
claims.put("lastName", new Claim());
claims.put("email", new Claim());
String jsonString = claims.toJsonString();
JsonNode jsonNode = JsonSerialization.mapper.readTree(jsonString);
assertNotNull(jsonNode.get("firstName"));
assertNotNull(jsonNode.get("lastName"));
assertNotNull(jsonNode.get("email"));
}
@Test
public void fromJsonString() {
final String serializeForm = "{ \"firstName\": {}, \"lastName\": {}, \"email\": {} }";
Claims claims = Claims.fromJsonString(serializeForm);
assertNotNull(claims);
assertNotNull(claims.get("firstName"));
assertNotNull(claims.get("lastName"));
assertNotNull(claims.get("email"));
}
@Test
public void fromJsonStringDeepClaim() {
final String serializeForm = "{ \"firstName\": {\"mandatory\":false}, \"lastName\": {\"mandatory\":false}, \"email\": {\"mandatory\":true} }";
Claims claims = Claims.fromJsonString(serializeForm);
assertNotNull(claims);
assertNotNull(claims.get("firstName"));
assertFalse(claims.get("firstName").getMandatory());
assertNotNull(claims.get("lastName"));
assertFalse(claims.get("lastName").getMandatory());
assertNotNull(claims.get("email"));
assertTrue(claims.get("email").getMandatory());
}
}

View file

@ -29,6 +29,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class OID4VCIssuerWellKnownProviderTest extends OID4VCTest { public class OID4VCIssuerWellKnownProviderTest extends OID4VCTest {
@ -52,6 +54,16 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCTest {
assertTrue("The test-credential should be supported.", credentialIssuer.getCredentialsSupported().containsKey("test-credential")); assertTrue("The test-credential should be supported.", credentialIssuer.getCredentialsSupported().containsKey("test-credential"));
assertEquals("The test-credential should offer type VerifiableCredential", "VerifiableCredential", credentialIssuer.getCredentialsSupported().get("test-credential").getScope()); assertEquals("The test-credential should offer type VerifiableCredential", "VerifiableCredential", credentialIssuer.getCredentialsSupported().get("test-credential").getScope());
assertEquals("The test-credential should be offered in the jwt-vc format.", Format.JWT_VC, credentialIssuer.getCredentialsSupported().get("test-credential").getFormat()); assertEquals("The test-credential should be offered in the jwt-vc format.", Format.JWT_VC, credentialIssuer.getCredentialsSupported().get("test-credential").getFormat());
assertNotNull("The test-credential can optionally provide a claims claim.", credentialIssuer.getCredentialsSupported().get("test-credential").getClaims());
assertNotNull("The test-credential claim firstName is present.", credentialIssuer.getCredentialsSupported().get("test-credential").getClaims().get("firstName"));
assertFalse("The test-credential claim firstName is not mandatory.", credentialIssuer.getCredentialsSupported().get("test-credential").getClaims().get("firstName").getMandatory());
assertEquals("The test-credential claim firstName shall be displayed as First Name", "First Name", credentialIssuer.getCredentialsSupported().get("test-credential").getClaims().get("firstName").getDisplay().get(0).getName());
assertEquals("The test-credential should offer vct VerifiableCredential", "VerifiableCredential", credentialIssuer.getCredentialsSupported().get("test-credential").getVct());
assertTrue("The test-credential should contain a cryptographic binding method supported named jwk", credentialIssuer.getCredentialsSupported().get("test-credential").getCryptographicBindingMethodsSupported().contains("jwk"));
assertTrue("The test-credential should contain a credential signing algorithm named ES256", credentialIssuer.getCredentialsSupported().get("test-credential").getCredentialSigningAlgValuesSupported().contains("ES256"));
assertTrue("The test-credential should contain a credential signing algorithm named ES384", credentialIssuer.getCredentialsSupported().get("test-credential").getCredentialSigningAlgValuesSupported().contains("ES384"));
assertEquals("The test-credential should display as Test Credential", "Test Credential", credentialIssuer.getCredentialsSupported().get("test-credential").getDisplay().get(0).getName());
assertTrue("The test-credential should support a proof of type jwt with signing algorithm ES256", credentialIssuer.getCredentialsSupported().get("test-credential").getProofTypesSupported().getJwt().getProofSigningAlgValuesSupported().contains("ES256"));
})); }));
} }

View file

@ -182,7 +182,14 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
clientRepresentation.setAttributes(Map.of( clientRepresentation.setAttributes(Map.of(
"vc.test-credential.expiry_in_s", "100", "vc.test-credential.expiry_in_s", "100",
"vc.test-credential.format", Format.JWT_VC.toString(), "vc.test-credential.format", Format.JWT_VC.toString(),
"vc.test-credential.scope", "VerifiableCredential")); "vc.test-credential.scope", "VerifiableCredential",
"vc.test-credential.claims", "{ \"firstName\": {\"mandatory\": false, \"display\": [{\"name\": \"First Name\", \"locale\": \"en-US\"}, {\"name\": \"名前\", \"locale\": \"ja-JP\"}]}, \"lastName\": {\"mandatory\": false}, \"email\": {\"mandatory\": false} }",
"vc.test-credential.vct", "VerifiableCredential",
"vc.test-credential.cryptographic_binding_methods_supported", "jwk",
"vc.test-credential.credential_signing_alg_values_supported", "ES256,ES384",
"vc.test-credential.display.0","{\n \"name\": \"Test Credential\"\n}",
"vc.test-credential.proof_types_supported","{\"jwt\":{\"proof_signing_alg_values_supported\":[\"ES256\"]}}"
));
clientRepresentation.setProtocolMappers( clientRepresentation.setProtocolMappers(
List.of( List.of(
getRoleMapper(clientId), getRoleMapper(clientId),
@ -347,4 +354,4 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
} }
} }
} }