diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/Claim.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/Claim.java new file mode 100644 index 0000000000..e66c568729 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/Claim.java @@ -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. + *

+ * See: openid-4-verifiable-credential-issuance-1_0.html#appendix-A.2.2 + * + * @author Francis Pouatcha + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Claim { + @JsonProperty("mandatory") + private Boolean mandatory; + @JsonProperty("value_type") + private String valueType; + @JsonProperty("display") + private List 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 getDisplay() { + return display; + } + + public Claim setDisplay(List display) { + this.display = display; + return this; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/ClaimDisplay.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ClaimDisplay.java new file mode 100644 index 0000000000..83c9ca5a62 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ClaimDisplay.java @@ -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 Francis Pouatcha + */ +@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; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/Claims.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/Claims.java new file mode 100644 index 0000000000..308295a2de --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/Claims.java @@ -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 Francis Pouatcha + */ +public class Claims extends HashMap { + + 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); + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/DisplayObject.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/DisplayObject.java index a25d8155d7..df51baf4ad 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/model/DisplayObject.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/DisplayObject.java @@ -14,17 +14,15 @@ * 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.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.util.JsonSerialization; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.io.IOException; /** * Represents a DisplayObject, as used in the OID4VCI Credentials Issuer Metadata @@ -126,26 +124,20 @@ public class DisplayObject { return this; } - public Map toDotNotation() { - Map dotNotation = new HashMap<>(); - dotNotation.put(NAME_KEY, name); - dotNotation.put(LOCALE_KEY, locale); - dotNotation.put(LOGO_KEY, logo); - dotNotation.put(DESCRIPTION_KEY, description); - dotNotation.put(BG_COLOR_KEY, backgroundColor); - dotNotation.put(TEXT_COLOR_KEY, textColor); - return dotNotation; + public String toJsonString(){ + try { + return JsonSerialization.writeValueAsString(this); + } catch (IOException e) { + throw new RuntimeException(e); + } } - public static DisplayObject fromDotNotation(Map dotNotated) { - DisplayObject displayObject = new DisplayObject(); - Optional.ofNullable(dotNotated.get(NAME_KEY)).ifPresent(displayObject::setName); - Optional.ofNullable(dotNotated.get(LOCALE_KEY)).ifPresent(displayObject::setLocale); - Optional.ofNullable(dotNotated.get(LOGO_KEY)).ifPresent(displayObject::setLogo); - 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; + public static DisplayObject fromJsonString(String jsonString){ + try { + return JsonSerialization.readValue(jsonString, DisplayObject.class); + } catch (IOException e) { + throw new RuntimeException(e); + } } @Override @@ -173,4 +165,4 @@ public class DisplayObject { result = 31 * result + (getTextColor() != null ? getTextColor().hashCode() : 0); return result; } -} \ No newline at end of file +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeCWT.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeCWT.java new file mode 100644 index 0000000000..dd06b1a03b --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeCWT.java @@ -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 Francis Pouatcha + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProofTypeCWT { + + @JsonProperty("proof_signing_alg_values_supported") + private List proofSigningAlgValuesSupported; + + @JsonProperty("proof_alg_values_supported") + private List proofAlgValuesSupported; + + @JsonProperty("proof_crv_values_supported") + private List proofCrvValuesSupported; + + public List getProofSigningAlgValuesSupported() { + return proofSigningAlgValuesSupported; + } + + public ProofTypeCWT setProofSigningAlgValuesSupported(List proofSigningAlgValuesSupported) { + this.proofSigningAlgValuesSupported = proofSigningAlgValuesSupported; + return this; + } + + public List getProofAlgValuesSupported() { + return proofAlgValuesSupported; + } + + public ProofTypeCWT setProofAlgValuesSupported(List proofAlgValuesSupported) { + this.proofAlgValuesSupported = proofAlgValuesSupported; + return this; + } + + public List getProofCrvValuesSupported() { + return proofCrvValuesSupported; + } + + public ProofTypeCWT setProofCrvValuesSupported(List 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); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeJWT.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeJWT.java new file mode 100644 index 0000000000..cf923e37fc --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeJWT.java @@ -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 Francis Pouatcha + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProofTypeJWT { + @JsonProperty("proof_signing_alg_values_supported") + private List proofSigningAlgValuesSupported; + + public List getProofSigningAlgValuesSupported() { + return proofSigningAlgValuesSupported; + } + + public ProofTypeJWT setProofSigningAlgValuesSupported(List 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); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeLdpVp.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeLdpVp.java new file mode 100644 index 0000000000..7de5c83a9f --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypeLdpVp.java @@ -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 Francis Pouatcha + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProofTypeLdpVp { +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypesSupported.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypesSupported.java new file mode 100644 index 0000000000..d232ff35ee --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/ProofTypesSupported.java @@ -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 Francis Pouatcha + */ +@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); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java index c15b8369cc..bef34c1b88 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java @@ -14,7 +14,6 @@ * 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.JsonIgnore; @@ -26,7 +25,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; /** * A supported credential, as used in the Credentials Issuer Metadata in OID4VCI @@ -42,13 +43,19 @@ public class SupportedCredentialConfiguration { @JsonIgnore private static final String SCOPE_KEY = "scope"; @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 private static final String CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY = "cryptographic_suites_supported"; @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 + private static final String VERIFIABLE_CREDENTIAL_TYPE_KEY = "vct"; private String id; @JsonProperty(FORMAT_KEY) @@ -67,7 +74,16 @@ public class SupportedCredentialConfiguration { private List credentialSigningAlgValuesSupported; @JsonProperty(DISPLAY_KEY) - private DisplayObject display; + private List 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() { return format; @@ -105,11 +121,11 @@ public class SupportedCredentialConfiguration { return this; } - public DisplayObject getDisplay() { + public List getDisplay() { return display; } - public SupportedCredentialConfiguration setDisplay(DisplayObject display) { + public SupportedCredentialConfiguration setDisplay(List display) { this.display = display; return this; } @@ -135,9 +151,37 @@ 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; + } + + 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 toDotNotation() { Map dotNotation = new HashMap<>(); 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(cryptographicBindingMethodsSupported).ifPresent(types -> 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))); Optional.ofNullable(cryptographicSuitesSupported).ifPresent(types -> 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 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; } @@ -159,6 +206,7 @@ public class SupportedCredentialConfiguration { 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 + 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 + CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY)) .map(cbms -> cbms.split(",")) @@ -172,45 +220,38 @@ public class SupportedCredentialConfiguration { .map(css -> css.split(",")) .map(Arrays::asList) .ifPresent(supportedCredentialConfiguration::setCredentialSigningAlgValuesSupported); - Map displayMap = new HashMap<>(); - dotNotated.entrySet().forEach(entry -> { - String key = entry.getKey(); - if (key.startsWith(credentialId + DOT_SEPARATOR + DISPLAY_KEY)) { - displayMap.put(key.substring((credentialId + DOT_SEPARATOR + DISPLAY_KEY).length() + 1), entry.getValue()); - } - }); - if (!displayMap.isEmpty()) { - supportedCredentialConfiguration.setDisplay(DisplayObject.fromDotNotation(displayMap)); + Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + CLAIMS_KEY)) + .map(Claims::fromJsonString) + .ifPresent(supportedCredentialConfiguration::setClaims); + + String displayKeyPrefix = credentialId + DOT_SEPARATOR + DISPLAY_KEY + DOT_SEPARATOR; + List displayList = dotNotated.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(displayKeyPrefix)) + .sorted(Map.Entry.comparingByKey()) + .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; } @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof SupportedCredentialConfiguration that)) return false; - - if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false; - 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; + if (o == null || getClass() != o.getClass()) return false; + SupportedCredentialConfiguration that = (SupportedCredentialConfiguration) o; + 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); } @Override public int hashCode() { - int result = getId() != null ? getId().hashCode() : 0; - 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; + return Objects.hash(id, format, scope, cryptographicBindingMethodsSupported, cryptographicSuitesSupported, credentialSigningAlgValuesSupported, display, vct, proofTypesSupported, claims); } -} \ No newline at end of file +} diff --git a/services/src/test/java/org/keycloak/protocol/oid4vc/OID4VCClientRegistrationProviderTest.java b/services/src/test/java/org/keycloak/protocol/oid4vc/OID4VCClientRegistrationProviderTest.java index 29736ffd18..b6caeff739 100644 --- a/services/src/test/java/org/keycloak/protocol/oid4vc/OID4VCClientRegistrationProviderTest.java +++ b/services/src/test/java/org/keycloak/protocol/oid4vc/OID4VCClientRegistrationProviderTest.java @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.keycloak.protocol.oid4vc; 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.Format; 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 java.util.Arrays; @@ -31,7 +32,6 @@ import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; @RunWith(Parameterized.class) public class OID4VCClientRegistrationProviderTest { @@ -68,13 +68,12 @@ public class OID4VCClientRegistrationProviderTest { Map.of( "vc.credential-id.format", Format.JWT_VC.toString(), "vc.credential-id.scope", "AnotherCredential", - "vc.credential-id.display.name", "Another", - "vc.credential-id.display.locale", "en"), + "vc.credential-id.display.0", "{\"name\":\"Another\",\"locale\":\"en\"}"), new OID4VCClient(null, "did:web:test.org", List.of(new SupportedCredentialConfiguration() .setId("credential-id") .setFormat(Format.JWT_VC) - .setDisplay(new DisplayObject().setLocale("en").setName("Another")) + .setDisplay(Arrays.asList(new DisplayObject().setLocale("en").setName("Another"))) .setScope("AnotherCredential")), null, null) }, @@ -83,23 +82,23 @@ public class OID4VCClientRegistrationProviderTest { Map.of( "vc.first-id.format", Format.JWT_VC.toString(), "vc.first-id.scope", "AnotherCredential", - "vc.first-id.display.name", "First", - "vc.first-id.display.locale", "en", + "vc.first-id.display.0", "{\"name\":\"First\",\"locale\":\"en\"}", "vc.second-id.format", Format.SD_JWT_VC.toString(), "vc.second-id.scope", "MyType", - "vc.second-id.display.name", "Second Credential", - "vc.second-id.display.locale", "de"), + "vc.second-id.display.0", "{\"name\":\"Second Credential\",\"locale\":\"de\"}", + "vc.second-id.proof_types_supported","{\"jwt\":{\"proof_signing_alg_values_supported\":[\"ES256\"]}}"), new OID4VCClient(null, "did:web:test.org", List.of(new SupportedCredentialConfiguration() .setId("first-id") .setFormat(Format.JWT_VC) - .setDisplay(new DisplayObject().setLocale("en").setName("First")) + .setDisplay(Arrays.asList(new DisplayObject().setLocale("en").setName("First"))) .setScope("AnotherCredential"), new SupportedCredentialConfiguration() .setId("second-id") .setFormat(Format.SD_JWT_VC) - .setDisplay(new DisplayObject().setLocale("de").setName("Second Credential")) - .setScope("MyType")), + .setDisplay(Arrays.asList(new DisplayObject().setLocale("de").setName("Second Credential"))) + .setScope("MyType") + .setProofTypesSupported(new ProofTypesSupported().setJwt(new ProofTypeJWT().setProofSigningAlgValuesSupported(Arrays.asList("ES256"))))), null, null) } }); @@ -129,4 +128,4 @@ public class OID4VCClientRegistrationProviderTest { OID4VCClientRegistrationProvider.fromClientAttributes("did:web:test.org", clientAttributes)); } -} \ No newline at end of file +} diff --git a/services/src/test/java/org/keycloak/protocol/oid4vc/model/ClaimsTest.java b/services/src/test/java/org/keycloak/protocol/oid4vc/model/ClaimsTest.java new file mode 100644 index 0000000000..a9d012eb2a --- /dev/null +++ b/services/src/test/java/org/keycloak/protocol/oid4vc/model/ClaimsTest.java @@ -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 Francis Pouatcha + */ +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()); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java index 2ee6127216..73129a0d99 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java @@ -29,6 +29,8 @@ import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; 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")); 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()); + 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")); })); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java index 3b4500cefb..954dda47b4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java @@ -182,7 +182,14 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest { clientRepresentation.setAttributes(Map.of( "vc.test-credential.expiry_in_s", "100", "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( List.of( getRoleMapper(clientId), @@ -347,4 +354,4 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest { } } -} \ No newline at end of file +}