[java-idp-plugin-vci] 01/01: More extensive parsing of issuer metadata. Validate pre-authorized credentials match issuer metadata for mandatory claims

Codeberg noreply at shibboleth.net
Tue Jul 28 13:37:53 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/CREDCONFIMPRV
in repository java-idp-plugin-vci.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/929c434cb4b4af20916a253f901a503d0115c30c

commit 929c434cb4b4af20916a253f901a503d0115c30c
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Tue Jul 28 16:37:29 2026 +0300

    More extensive parsing of issuer metadata. Validate pre-authorized credentials match issuer metadata for mandatory claims
---
 .../openidvci/credential/ClaimDescription.java     | 108 +++++++++++++
 .../plugin/openidvci/credential/ClaimDisplay.java  |  79 ++++++++++
 .../credential/CredentialConfiguration.java        |  83 +++++++++-
 .../openidvci/credential/CredentialDisplay.java    | 173 ++++++++++++++++++++
 .../openidvci/credential/CredentialMetadata.java   |  81 ++++++++++
 .../plugin/openidvci/credential/Image.java         |  80 ++++++++++
 .../credential/KeyAttestationsRequired.java        |  87 +++++++++++
 .../openidvci/credential/ProofTypeSupported.java   |  91 +++++++++++
 .../credential/CredentialConfigurationTest.java    | 105 +++++++++++++
 .../profile/impl/ValidateCredentialOffering.java   |  95 ++++++++++-
 .../impl/ValidateCredentialOfferingTest.java       |  69 +++++++-
 .../resources/conf/verifiable-credentials.json     | 174 +++++++++++----------
 12 files changed, 1127 insertions(+), 98 deletions(-)

diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ClaimDescription.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ClaimDescription.java
new file mode 100644
index 0000000..fc0642d
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ClaimDescription.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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.geant.shibboleth.plugin.openidvci.credential;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Description of a single claim inside {@link CredentialMetadata#getClaims()}
+ * as defined in
+ * {@link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#claims-description-issuer-metadata}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class ClaimDescription {
+
+    /**
+     * JSON path locating the claim within the issued Credential. Each element is
+     * either a string (property name), a non-negative integer (array index), or
+     * null (all elements). Modeled as {@link Object} to accept all three.
+     */
+    @Nonnull
+    @JsonProperty("path")
+    private final List<Object> path;
+
+    /** Display properties for this claim per language. */
+    @Nullable
+    @JsonProperty("display")
+    @JsonInclude(Include.NON_ABSENT)
+    private final List<ClaimDisplay> display;
+
+    /** Whether Credential Issuer will always include this claim in the issued Credential. */
+    @Nullable
+    @JsonProperty("mandatory")
+    @JsonInclude(Include.NON_NULL)
+    private final Boolean mandatory;
+
+    /**
+     * Constructor.
+     *
+     * @param path      JSON path locating the claim within the issued Credential
+     * @param display   Display properties for this claim per language
+     * @param mandatory Whether Credential Issuer will always include this claim in
+     *                  the issued Credential
+     */
+    @JsonCreator
+    private ClaimDescription(@JsonProperty("path") @Nonnull List<Object> path,
+            @JsonProperty("display") @Nullable List<ClaimDisplay> display,
+            @JsonProperty("mandatory") @Nullable Boolean mandatory) {
+        assert path != null;
+        this.path = path;
+        this.display = display;
+        this.mandatory = mandatory;
+    }
+
+    /**
+     * Get JSON path locating the claim within the issued Credential.
+     *
+     * @return JSON path locating the claim within the issued Credential
+     */
+    @Nonnull
+    public List<Object> getPath() {
+        return path;
+    }
+
+    /**
+     * Get display properties for this claim per language.
+     *
+     * @return Display properties for this claim per language
+     */
+    @Nullable
+    public List<ClaimDisplay> getDisplay() {
+        return display;
+    }
+
+    /**
+     * Get whether Credential Issuer will always include this claim in the issued
+     * Credential.
+     *
+     * @return Whether Credential Issuer will always include this claim in the
+     *         issued Credential
+     */
+    @Nullable
+    public Boolean getMandatory() {
+        return mandatory;
+    }
+}
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ClaimDisplay.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ClaimDisplay.java
new file mode 100644
index 0000000..edf5459
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ClaimDisplay.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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.geant.shibboleth.plugin.openidvci.credential;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Display properties of a single claim for a particular language, used inside
+ * {@link ClaimDescription#getDisplay()} as defined in
+ * {@link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#claims-description-issuer-metadata}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class ClaimDisplay {
+
+    /** Display name of the claim. */
+    @Nullable
+    @JsonProperty("name")
+    @JsonInclude(Include.NON_NULL)
+    private final String name;
+
+    /** Language tag (BCP47) identifying the language of this object. */
+    @Nullable
+    @JsonProperty("locale")
+    @JsonInclude(Include.NON_NULL)
+    private final String locale;
+
+    /**
+     * Constructor.
+     *
+     * @param name   Display name of the claim
+     * @param locale Language tag identifying the language of this object
+     */
+    @JsonCreator
+    private ClaimDisplay(@JsonProperty("name") @Nullable String name,
+            @JsonProperty("locale") @Nullable String locale) {
+        this.name = name;
+        this.locale = locale;
+    }
+
+    /**
+     * Get display name of the claim.
+     *
+     * @return Display name of the claim
+     */
+    @Nullable
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Get language tag identifying the language of this object.
+     *
+     * @return Language tag identifying the language of this object
+     */
+    @Nullable
+    public String getLocale() {
+        return locale;
+    }
+}
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfiguration.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfiguration.java
index 4ddad53..467f5db 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfiguration.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfiguration.java
@@ -17,6 +17,7 @@
 package org.geant.shibboleth.plugin.openidvci.credential;
 
 import java.util.List;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -34,8 +35,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
  * 
  * Credential configurations are defined in
  * {@link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p}
- * as parameter credential_configurations_supported. Still only a partial
- * implementation of the parameters.
+ * as parameter credential_configurations_supported.
  */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class CredentialConfiguration {
@@ -79,9 +79,32 @@ public class CredentialConfiguration {
     @JsonInclude(Include.NON_NULL)
     private final CredentialDefinition credentialDefinition;
 
+    /**
+     * Specifics of the key proof(s) that the Credential Issuer supports
+     */
+    @Nullable
+    @JsonProperty("proof_types_supported")
+    @JsonInclude(Include.NON_ABSENT)
+    private final Map<String, ProofTypeSupported> proofTypesSupported;
+
+    /** Information relevant to the usage and display of issued Credentials. */
+    @Nullable
+    @JsonProperty("credential_metadata")
+    @JsonInclude(Include.NON_NULL)
+    private final CredentialMetadata credentialMetadata;
+
+    /**
+     * Verifiable Credential Type identifier. Required for SD-JWT VC formats
+     * ({@code dc+sd-jwt} / {@code vc+sd-jwt}).
+     */
+    @Nullable
+    @JsonProperty("vct")
+    @JsonInclude(Include.NON_NULL)
+    private final String vct;
+
     /**
      * Constructor.
-     * 
+     *
      * @param format                               Identifies the format of this
      *                                             Credential
      * @param scope                                Identifies the scope value that
@@ -99,24 +122,42 @@ public class CredentialConfiguration {
      * @param credentialDefinition                 Object containing the detailed
      *                                             description of the Credential
      *                                             type
+     * @param proofTypesSupported                  Specifics of the key proof(s)
+     *                                             that the Credential Issuer
+     *                                             supports
+     * @param credentialMetadata                   Information relevant to the
+     *                                             usage and display of issued
+     *                                             Credentials
+     * @param vct                                  Verifiable Credential Type
+     *                                             identifier (SD-JWT VC formats)
      */
     @JsonCreator
     private CredentialConfiguration(@JsonProperty("format") @Nonnull String format,
             @JsonProperty("scope") @Nullable String scope,
             @JsonProperty("credential_signing_alg_values_supported") @Nullable List<String> credentialSigningAlgValuesSupported,
             @JsonProperty("cryptographic_binding_methods_supported") @Nullable List<String> cryptographicBindingMethodsSupported,
-            @JsonProperty("credential_definition") @Nullable CredentialDefinition credentialDefinition) {
+            @JsonProperty("credential_definition") @Nullable CredentialDefinition credentialDefinition,
+            @JsonProperty("proof_types_supported") @Nullable Map<String, ProofTypeSupported> proofTypesSupported,
+            @JsonProperty("credential_metadata") @Nullable CredentialMetadata credentialMetadata,
+            @JsonProperty("vct") @Nullable String vct) {
         assert format != null;
         this.format = format;
         this.scope = scope;
         this.credentialSigningAlgValuesSupported = credentialSigningAlgValuesSupported;
         this.cryptographicBindingMethodsSupported = cryptographicBindingMethodsSupported;
         this.credentialDefinition = credentialDefinition;
+        this.proofTypesSupported = proofTypesSupported;
+        this.credentialMetadata = credentialMetadata;
+        this.vct = vct;
         // W3C family of credentials require credential definition
         if (credentialDefinition == null
-                && ("jwt_vc_json-ld".equals(format) || "jwt_vc_json".equals(format) || "ldp_vc".equals(format))) {
+                && "vc+sd-jwt".equals(format)) {
             throw new IllegalArgumentException("credential_definition is missing");
         }
+        // SD-JWT VC family requires vct
+        if (vct == null && "dc+sd-jwt".equals(format)) {
+            throw new IllegalArgumentException("vct is missing");
+        }
     }
 
     /**
@@ -168,7 +209,7 @@ public class CredentialConfiguration {
 
     /**
      * Get object containing the detailed description of the Credential type.
-     * 
+     *
      * @return Object containing the detailed description of the Credential type
      */
     @Nullable
@@ -176,6 +217,36 @@ public class CredentialConfiguration {
         return credentialDefinition;
     }
 
+    /**
+     * Get specifics of the key proof(s) that the Credential Issuer supports.
+     *
+     * @return Specifics of the key proof(s) that the Credential Issuer supports
+     */
+    @Nullable
+    public Map<String, ProofTypeSupported> getProofTypesSupported() {
+        return proofTypesSupported;
+    }
+
+    /**
+     * Get information relevant to the usage and display of issued Credentials.
+     *
+     * @return Information relevant to the usage and display of issued Credentials
+     */
+    @Nullable
+    public CredentialMetadata getCredentialMetadata() {
+        return credentialMetadata;
+    }
+
+    /**
+     * Get Verifiable Credential Type identifier (SD-JWT VC formats).
+     *
+     * @return Verifiable Credential Type identifier
+     */
+    @Nullable
+    public String getVct() {
+        return vct;
+    }
+
     /**
      * Serialize object to json.
      * 
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDisplay.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDisplay.java
new file mode 100644
index 0000000..34dbdf5
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDisplay.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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.geant.shibboleth.plugin.openidvci.credential;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Display properties of a supported Credential for a particular language, used
+ * inside {@link CredentialMetadata#getDisplay()} as defined in
+ * {@link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class CredentialDisplay {
+
+    /** Display name of the Credential. */
+    @Nullable
+    @JsonProperty("name")
+    @JsonInclude(Include.NON_NULL)
+    private final String name;
+
+    /** Language tag (BCP47) identifying the language of this object. */
+    @Nullable
+    @JsonProperty("locale")
+    @JsonInclude(Include.NON_NULL)
+    private final String locale;
+
+    /** Logo of the Credential. */
+    @Nullable
+    @JsonProperty("logo")
+    @JsonInclude(Include.NON_NULL)
+    private final Image logo;
+
+    /** Description of the Credential. */
+    @Nullable
+    @JsonProperty("description")
+    @JsonInclude(Include.NON_NULL)
+    private final String description;
+
+    /** Background color of the Credential (CSS color value). */
+    @Nullable
+    @JsonProperty("background_color")
+    @JsonInclude(Include.NON_NULL)
+    private final String backgroundColor;
+
+    /** Background image of the Credential. */
+    @Nullable
+    @JsonProperty("background_image")
+    @JsonInclude(Include.NON_NULL)
+    private final Image backgroundImage;
+
+    /** Text color of the Credential (CSS color value). */
+    @Nullable
+    @JsonProperty("text_color")
+    @JsonInclude(Include.NON_NULL)
+    private final String textColor;
+
+    /**
+     * Constructor.
+     *
+     * @param name            Display name of the Credential
+     * @param locale          Language tag identifying the language of this object
+     * @param logo            Logo of the Credential
+     * @param description     Description of the Credential
+     * @param backgroundColor Background color of the Credential
+     * @param backgroundImage Background image of the Credential
+     * @param textColor       Text color of the Credential
+     */
+    @JsonCreator
+    private CredentialDisplay(@JsonProperty("name") @Nullable String name,
+            @JsonProperty("locale") @Nullable String locale, @JsonProperty("logo") @Nullable Image logo,
+            @JsonProperty("description") @Nullable String description,
+            @JsonProperty("background_color") @Nullable String backgroundColor,
+            @JsonProperty("background_image") @Nullable Image backgroundImage,
+            @JsonProperty("text_color") @Nullable String textColor) {
+        this.name = name;
+        this.locale = locale;
+        this.logo = logo;
+        this.description = description;
+        this.backgroundColor = backgroundColor;
+        this.backgroundImage = backgroundImage;
+        this.textColor = textColor;
+    }
+
+    /**
+     * Get display name of the Credential.
+     *
+     * @return Display name of the Credential
+     */
+    @Nullable
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Get language tag identifying the language of this object.
+     *
+     * @return Language tag identifying the language of this object
+     */
+    @Nullable
+    public String getLocale() {
+        return locale;
+    }
+
+    /**
+     * Get logo of the Credential.
+     *
+     * @return Logo of the Credential
+     */
+    @Nullable
+    public Image getLogo() {
+        return logo;
+    }
+
+    /**
+     * Get description of the Credential.
+     *
+     * @return Description of the Credential
+     */
+    @Nullable
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Get background color of the Credential.
+     *
+     * @return Background color of the Credential
+     */
+    @Nullable
+    public String getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    /**
+     * Get background image of the Credential.
+     *
+     * @return Background image of the Credential
+     */
+    @Nullable
+    public Image getBackgroundImage() {
+        return backgroundImage;
+    }
+
+    /**
+     * Get text color of the Credential.
+     *
+     * @return Text color of the Credential
+     */
+    @Nullable
+    public String getTextColor() {
+        return textColor;
+    }
+}
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialMetadata.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialMetadata.java
new file mode 100644
index 0000000..b42cbc1
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialMetadata.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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.geant.shibboleth.plugin.openidvci.credential;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Credential metadata parameter of a {@link CredentialConfiguration} as defined
+ * in
+ * {@link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class CredentialMetadata {
+
+    /** Display properties per language. */
+    @Nullable
+    @JsonProperty("display")
+    @JsonInclude(Include.NON_ABSENT)
+    private final List<CredentialDisplay> display;
+
+    /** Description of the claims carried by this Credential. */
+    @Nullable
+    @JsonProperty("claims")
+    @JsonInclude(Include.NON_ABSENT)
+    private final List<ClaimDescription> claims;
+
+    /**
+     * Constructor.
+     *
+     * @param display Display properties per language
+     * @param claims  Description of the claims carried by this Credential
+     */
+    @JsonCreator
+    private CredentialMetadata(@JsonProperty("display") @Nullable List<CredentialDisplay> display,
+            @JsonProperty("claims") @Nullable List<ClaimDescription> claims) {
+        this.display = display;
+        this.claims = claims;
+    }
+
+    /**
+     * Get display properties per language.
+     *
+     * @return Display properties per language
+     */
+    @Nullable
+    public List<CredentialDisplay> getDisplay() {
+        return display;
+    }
+
+    /**
+     * Get description of the claims carried by this Credential.
+     *
+     * @return Description of the claims carried by this Credential
+     */
+    @Nullable
+    public List<ClaimDescription> getClaims() {
+        return claims;
+    }
+}
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/Image.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/Image.java
new file mode 100644
index 0000000..eacb671
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/Image.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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.geant.shibboleth.plugin.openidvci.credential;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Image reference used by {@link CredentialDisplay#getLogo()} and
+ * {@link CredentialDisplay#getBackgroundImage()}. The spec defines
+ * {@code alt_text} only for {@code logo}; leaving it {@code null} yields a
+ * valid {@code background_image} object.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class Image {
+
+    /** URI where the Wallet can obtain the image from the Credential Issuer. */
+    @Nonnull
+    @JsonProperty("uri")
+    private final String uri;
+
+    /** Alternative text for the image (logo only per the spec). */
+    @Nullable
+    @JsonProperty("alt_text")
+    @JsonInclude(Include.NON_NULL)
+    private final String altText;
+
+    /**
+     * Constructor.
+     *
+     * @param uri     URI where the Wallet can obtain the image
+     * @param altText Alternative text for the image
+     */
+    @JsonCreator
+    private Image(@JsonProperty("uri") @Nonnull String uri, @JsonProperty("alt_text") @Nullable String altText) {
+        assert uri != null;
+        this.uri = uri;
+        this.altText = altText;
+    }
+
+    /**
+     * Get URI where the Wallet can obtain the image.
+     *
+     * @return URI where the Wallet can obtain the image
+     */
+    @Nonnull
+    public String getUri() {
+        return uri;
+    }
+
+    /**
+     * Get alternative text for the image.
+     *
+     * @return Alternative text for the image
+     */
+    @Nullable
+    public String getAltText() {
+        return altText;
+    }
+}
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/KeyAttestationsRequired.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/KeyAttestationsRequired.java
new file mode 100644
index 0000000..694c974
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/KeyAttestationsRequired.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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.geant.shibboleth.plugin.openidvci.credential;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Key attestations required inside {@link ProofTypeSupported} as defined in
+ * {@link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-key-attestations-in-jwt-pro}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class KeyAttestationsRequired {
+
+    /**
+     * Resistance level required for the key storage component of the attested key.
+     */
+    @Nullable
+    @JsonProperty("key_storage")
+    @JsonInclude(Include.NON_ABSENT)
+    private final List<String> keyStorage;
+
+    /**
+     * Resistance level required for the user authentication component of the
+     * attested key.
+     */
+    @Nullable
+    @JsonProperty("user_authentication")
+    @JsonInclude(Include.NON_ABSENT)
+    private final List<String> userAuthentication;
+
+    /**
+     * Constructor.
+     *
+     * @param keyStorage         Resistance levels the key storage component must
+     *                           satisfy
+     * @param userAuthentication Resistance levels the user authentication component
+     *                           must satisfy
+     */
+    @JsonCreator
+    private KeyAttestationsRequired(@JsonProperty("key_storage") @Nullable List<String> keyStorage,
+            @JsonProperty("user_authentication") @Nullable List<String> userAuthentication) {
+        this.keyStorage = keyStorage;
+        this.userAuthentication = userAuthentication;
+    }
+
+    /**
+     * Get resistance levels the key storage component must satisfy.
+     *
+     * @return Resistance levels the key storage component must satisfy
+     */
+    @Nullable
+    public List<String> getKeyStorage() {
+        return keyStorage;
+    }
+
+    /**
+     * Get resistance levels the user authentication component must satisfy.
+     *
+     * @return Resistance levels the user authentication component must satisfy
+     */
+    @Nullable
+    public List<String> getUserAuthentication() {
+        return userAuthentication;
+    }
+}
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ProofTypeSupported.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ProofTypeSupported.java
new file mode 100644
index 0000000..ddef74a
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/ProofTypeSupported.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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.geant.shibboleth.plugin.openidvci.credential;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Metadata about a single key proof type advertised in
+ * {@link CredentialConfiguration#getProofTypesSupported()} as defined in
+ * {@link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class ProofTypeSupported {
+
+    /**
+     * Algorithm identifiers the Issuer supports for this proof type.
+     */
+    @Nonnull
+    @JsonProperty("proof_signing_alg_values_supported")
+    private final List<String> proofSigningAlgValuesSupported;
+
+    /**
+     * Key attestation requirements the Wallet must satisfy for this proof type.
+     */
+    @Nullable
+    @JsonProperty("key_attestations_required")
+    @JsonInclude(Include.NON_NULL)
+    private final KeyAttestationsRequired keyAttestationsRequired;
+
+    /**
+     * Constructor.
+     *
+     * @param proofSigningAlgValuesSupported Algorithm identifiers the Issuer
+     *                                       supports for this proof type
+     * @param keyAttestationsRequired        Key attestation requirements the Wallet
+     *                                       must satisfy for this proof type
+     */
+    @JsonCreator
+    private ProofTypeSupported(
+            @JsonProperty("proof_signing_alg_values_supported") @Nonnull List<String> proofSigningAlgValuesSupported,
+            @JsonProperty("key_attestations_required") @Nullable KeyAttestationsRequired keyAttestationsRequired) {
+        assert proofSigningAlgValuesSupported != null;
+        this.proofSigningAlgValuesSupported = proofSigningAlgValuesSupported;
+        this.keyAttestationsRequired = keyAttestationsRequired;
+    }
+
+    /**
+     * Get algorithm identifiers the Issuer supports for this proof type.
+     *
+     * @return Algorithm identifiers the Issuer supports for this proof type
+     */
+    @Nonnull
+    public List<String> getProofSigningAlgValuesSupported() {
+        return proofSigningAlgValuesSupported;
+    }
+
+    /**
+     * Get key attestation requirements the Wallet must satisfy for this proof
+     * type.
+     *
+     * @return Key attestation requirements the Wallet must satisfy for this proof
+     *         type
+     */
+    @Nullable
+    public KeyAttestationsRequired getKeyAttestationsRequired() {
+        return keyAttestationsRequired;
+    }
+}
diff --git a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationTest.java b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationTest.java
index 211f5e8..99b33bf 100644
--- a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationTest.java
+++ b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationTest.java
@@ -17,6 +17,7 @@
 package org.geant.shibboleth.plugin.openidvci.credential;
 
 import java.io.IOException;
+import java.util.List;
 
 import org.springframework.core.io.ClassPathResource;
 import org.testng.Assert;
@@ -52,6 +53,29 @@ public class CredentialConfigurationTest {
         Assert.assertEquals(configuration.getCredentialSigningAlgValuesSupported().get(0), "ES256");
         Assert.assertEquals(configuration.getCryptographicBindingMethodsSupported().get(0), "jwk");
 
+        ProofTypeSupported jwtProof = configuration.getProofTypesSupported().get("jwt");
+        Assert.assertNotNull(jwtProof);
+        Assert.assertEquals(jwtProof.getProofSigningAlgValuesSupported().get(0), "ES256");
+        Assert.assertEquals(jwtProof.getKeyAttestationsRequired().getKeyStorage().get(0), "iso_18045_moderate");
+        Assert.assertEquals(jwtProof.getKeyAttestationsRequired().getUserAuthentication().get(0),
+                "iso_18045_moderate");
+
+        Assert.assertEquals(configuration.getVct(), "SD_JWT_VC_example_in_OpenID4VCI");
+
+        CredentialDisplay display = configuration.getCredentialMetadata().getDisplay().get(0);
+        Assert.assertEquals(display.getName(), "IdentityCredential");
+        Assert.assertEquals(display.getLocale(), "en-US");
+        Assert.assertEquals(display.getLogo().getUri(), "https://university.example.edu/public/logo.png");
+        Assert.assertEquals(display.getLogo().getAltText(), "a square logo of a university");
+        Assert.assertEquals(display.getBackgroundColor(), "#12107c");
+        Assert.assertEquals(display.getTextColor(), "#FFFFFF");
+
+        List<ClaimDescription> claims = configuration.getCredentialMetadata().getClaims();
+        Assert.assertEquals(claims.get(0).getPath().get(0), "given_name");
+        Assert.assertEquals(claims.get(0).getDisplay().get(0).getName(), "Given Name");
+        Assert.assertEquals(claims.get(0).getDisplay().get(0).getLocale(), "en-US");
+        Assert.assertEquals(claims.get(0).getDisplay().get(1).getName(), "Vorname");
+        Assert.assertNull(claims.get(0).getMandatory());
     }
 
     @Test
@@ -65,7 +89,88 @@ public class CredentialConfigurationTest {
         Assert.assertNull(configuration.getScope());
         Assert.assertEquals(configuration.getCredentialSigningAlgValuesSupported().get(0), "Ed25519Signature2018");
         Assert.assertEquals(configuration.getCryptographicBindingMethodsSupported().get(0), "did:example");
+        Assert.assertNull(configuration.getProofTypesSupported());
 
+        Assert.assertNull(configuration.getVct());
+
+        CredentialDisplay display = configuration.getCredentialMetadata().getDisplay().get(0);
+        Assert.assertEquals(display.getName(), "University Credential");
+        Assert.assertEquals(display.getLocale(), "en-US");
+        Assert.assertEquals(display.getLogo().getUri(), "https://university.example.edu/public/logo.png");
+
+        List<ClaimDescription> claims = configuration.getCredentialMetadata().getClaims();
+        Assert.assertEquals(claims.get(3).getPath().get(0), "credentialSubject");
+        Assert.assertEquals(claims.get(3).getPath().get(1), "gpa");
+        Assert.assertEquals(claims.get(3).getMandatory(), Boolean.TRUE);
+        Assert.assertEquals(claims.get(3).getDisplay().get(0).getName(), "GPA");
+    }
+
+    @Test
+    public void parseMinimalTopLevel() throws JsonProcessingException {
+        CredentialConfiguration configuration = CredentialConfiguration
+                .parse("{\"format\":\"dc+sd-jwt\",\"vct\":\"MinimalType\"}");
+        Assert.assertEquals(configuration.getFormat(), "dc+sd-jwt");
+        Assert.assertEquals(configuration.getVct(), "MinimalType");
+        Assert.assertNull(configuration.getScope());
+        Assert.assertNull(configuration.getCredentialSigningAlgValuesSupported());
+        Assert.assertNull(configuration.getCryptographicBindingMethodsSupported());
+        Assert.assertNull(configuration.getCredentialDefinition());
+        Assert.assertNull(configuration.getProofTypesSupported());
+        Assert.assertNull(configuration.getCredentialMetadata());
+    }
+
+    @Test
+    public void parseMinimalNested() throws JsonProcessingException {
+        String json = "{\"format\":\"dc+sd-jwt\",\"vct\":\"MinimalType\","
+                + "\"proof_types_supported\":{\"jwt\":{\"proof_signing_alg_values_supported\":[\"ES256\"]}},"
+                + "\"credential_metadata\":{\"display\":[{\"name\":\"Minimal\","
+                + "\"background_image\":{\"uri\":\"https://example.org/bg.png\"}}],"
+                + "\"claims\":[{\"path\":[\"given_name\"]}]}}";
+        CredentialConfiguration configuration = CredentialConfiguration.parse(json);
+
+        ProofTypeSupported jwtProof = configuration.getProofTypesSupported().get("jwt");
+        Assert.assertNotNull(jwtProof);
+        Assert.assertEquals(jwtProof.getProofSigningAlgValuesSupported().get(0), "ES256");
+        Assert.assertNull(jwtProof.getKeyAttestationsRequired());
+
+        CredentialDisplay display = configuration.getCredentialMetadata().getDisplay().get(0);
+        Assert.assertEquals(display.getName(), "Minimal");
+        Assert.assertNull(display.getLocale());
+        Assert.assertNull(display.getLogo());
+        Assert.assertNull(display.getDescription());
+        Assert.assertNull(display.getBackgroundColor());
+        Assert.assertNull(display.getTextColor());
+        Assert.assertEquals(display.getBackgroundImage().getUri(), "https://example.org/bg.png");
+        Assert.assertNull(display.getBackgroundImage().getAltText());
+
+        ClaimDescription claim = configuration.getCredentialMetadata().getClaims().get(0);
+        Assert.assertEquals(claim.getPath().get(0), "given_name");
+        Assert.assertNull(claim.getDisplay());
+        Assert.assertNull(claim.getMandatory());
+    }
+
+    @Test(expectedExceptions = JsonProcessingException.class)
+    public void parseSDJWTMissingVctThrows() throws JsonProcessingException {
+        CredentialConfiguration.parse("{\"format\":\"dc+sd-jwt\"}");
+    }
+
+    @Test
+    public void parseVCSDJWT() throws JsonProcessingException {
+        String json = "{\"format\":\"vc+sd-jwt\","
+                + "\"credential_definition\":{\"@context\":[\"https://example.org/ctx\"],"
+                + "\"type\":[\"VerifiableCredential\",\"IdentityCredential\"],"
+                + "\"issuer\":\"https://issuer.example.org\"}}";
+        CredentialConfiguration configuration = CredentialConfiguration.parse(json);
+        Assert.assertEquals(configuration.getFormat(), "vc+sd-jwt");
+        Assert.assertNotNull(configuration.getCredentialDefinition());
+        Assert.assertEquals(configuration.getCredentialDefinition().getType().get(1), "IdentityCredential");
+        Assert.assertEquals(configuration.getCredentialDefinition().getIssuer(), "https://issuer.example.org");
+        Assert.assertNull(configuration.getVct());
+    }
+
+    @Test(expectedExceptions = JsonProcessingException.class)
+    public void parseVCSDJWTMissingCredentialDefinitionThrows() throws JsonProcessingException {
+        CredentialConfiguration.parse("{\"format\":\"vc+sd-jwt\"}");
     }
 
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOffering.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOffering.java
index 43ef451..5e071d7 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOffering.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOffering.java
@@ -16,12 +16,19 @@
 
 package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
 
+import org.geant.shibboleth.plugin.openidvci.credential.ClaimDescription;
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialMetadata;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialOfferContext;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequest;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedClaim;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
 import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -31,14 +38,12 @@ import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 
 /**
  * Action validates that there is a credential offering matching the request.
- * For both pre-authorized and code flows we check only that credential
- * configuration id is a supported one. If validation is successful a
- * {@link CredentialOfferContext} is created and requested credentials and
- * possible tx code is stored there.
- *
- * TBD: verify that also claims in pre-authorized request match the claims in
- * supported configuration.
- * 
+ * For both pre-authorized and code flows the credential configuration id must
+ * be a supported one. Additionally, for the pre-authorized flow, every claim
+ * marked {@code mandatory: true} in the configuration's
+ * {@code credential_metadata.claims} must be present in the requested claims.
+ * If validation is successful a {@link CredentialOfferContext} is created and
+ * requested credentials and possible tx code is stored there.
  */
 public class ValidateCredentialOffering extends AbstractCredentialValidationAction {
 
@@ -86,6 +91,22 @@ public class ValidateCredentialOffering extends AbstractCredentialValidationActi
             }
         });
 
+        if (request.getPreAuthorizedCredentials() != null) {
+            for (Map.Entry<String, CredentialOfferRequestedCredential> entry : request.getPreAuthorizedCredentials()
+                    .entrySet()) {
+                final CredentialConfiguration configuration = getCredentialConfigurations().get(entry.getKey());
+                if (configuration == null) {
+                    continue;
+                }
+                if (!hasAllMandatoryClaims(configuration, entry.getValue())) {
+                    log.error("{} Requested credential {} is missing one or more mandatory claims", getLogPrefix(),
+                            entry.getKey());
+                    ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
+                    return;
+                }
+            }
+        }
+
         CredentialOfferContext ctx = new CredentialOfferContext();
         ctx.setCredentialConfigurations(getCredentialConfigurations());
         ctx.setValidatedPreAuthorizedCredentials(request.getPreAuthorizedCredentials());
@@ -93,4 +114,62 @@ public class ValidateCredentialOffering extends AbstractCredentialValidationActi
         ctx.setCredentialOfferTxCode(request.getCredentialOfferTxCode());
         profileRequestContext.getInboundMessageContext().addSubcontext(ctx, true);
     }
+
+    /**
+     * Check that every {@code mandatory: true} claim declared in the configuration
+     * is present in the requested claims.
+     *
+     * @param configuration       Credential configuration for the requested id
+     * @param requestedCredential Requested claims from the pre-authorized offer
+     * @return true if all mandatory claims are present, or if the configuration
+     *         declares none
+     */
+    private boolean hasAllMandatoryClaims(@Nonnull final CredentialConfiguration configuration,
+            @Nonnull final CredentialOfferRequestedCredential requestedCredential) {
+        final CredentialMetadata metadata = configuration.getCredentialMetadata();
+        if (metadata == null || metadata.getClaims() == null) {
+            return true;
+        }
+        for (ClaimDescription claim : metadata.getClaims()) {
+            if (!Boolean.TRUE.equals(claim.getMandatory())) {
+                continue;
+            }
+            if (!requestContainsPath(requestedCredential, claim.getPath())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Test whether any requested claim's path equals the given configuration path.
+     * Configuration paths are typed as {@link Object} per spec (element may be
+     * string, non-negative integer, or null); this comparison only matches string
+     * elements against the request's string-only path.
+     *
+     * @param requestedCredential Requested claims from the pre-authorized offer
+     * @param configPath          Path from a configuration claim
+     * @return true if a matching requested claim path exists
+     */
+    private boolean requestContainsPath(@Nonnull final CredentialOfferRequestedCredential requestedCredential,
+            @Nonnull final List<Object> configPath) {
+        for (CredentialOfferRequestedClaim requestedClaim : requestedCredential.getRequestedCredential()) {
+            final List<String> requestPath = requestedClaim.getPath();
+            if (configPath.size() != requestPath.size()) {
+                continue;
+            }
+            boolean match = true;
+            for (int i = 0; i < configPath.size(); i++) {
+                final Object configElement = configPath.get(i);
+                if (!(configElement instanceof String) || !configElement.equals(requestPath.get(i))) {
+                    match = false;
+                    break;
+                }
+            }
+            if (match) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOfferingTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOfferingTest.java
index 58dafa2..6e9c066 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOfferingTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateCredentialOfferingTest.java
@@ -1,5 +1,5 @@
 /*
-preAuthorizedCredentials * Copyright (c) 2025, GÉANT
+ * Copyright (c) 2025, GÉANT
  *
  * 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
@@ -104,4 +104,71 @@ public class ValidateCredentialOfferingTest {
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIAL_CONFIGURATION);
     }
 
+    @Test
+    public void testMissingMandatoryClaim() throws Exception {
+        HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        // Request omits the mandatory "mail" claim declared in the configuration.
+        httpRequest.setQuery(
+                "{\"preAuthorizedCredentials\":{\"GeantIncubatorDiploma\":[{\"path\":[\"givenName\"],\"value\":\"Gemma\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]}}");
+        profileRequestCtx.getInboundMessageContext().setMessage(CredentialOfferRequest.parse(httpRequest));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
+    }
+
+    @Test
+    public void testCodeFlowSuccess() throws Exception {
+        HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setQuery("{\"codeCredentials\":[\"GeantIncubatorDiploma\"]}");
+        profileRequestCtx.getInboundMessageContext().setMessage(CredentialOfferRequest.parse(httpRequest));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        CredentialOfferContext ctx = profileRequestCtx.getInboundMessageContext()
+                .getSubcontext(CredentialOfferContext.class);
+        Assert.notNull(ctx, "Must not be null");
+        Assert.notNull(ctx.getValidatedCodeCredentials(), "Must not be null");
+    }
+
+    @Test
+    public void testSuccessVcSdJwt() throws Exception {
+        HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setQuery(
+                "{\"preAuthorizedCredentials\":{\"GeantIncubatorDiploma2\":[{\"path\":[\"credentialSubject\",\"mail\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"credentialSubject\",\"givenName\"],\"value\":\"Gemma\"},{\"path\":[\"credentialSubject\",\"familyName\"],\"value\":\"Erasmus\"}]}}");
+        profileRequestCtx.getInboundMessageContext().setMessage(CredentialOfferRequest.parse(httpRequest));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        CredentialOfferContext ctx = profileRequestCtx.getInboundMessageContext()
+                .getSubcontext(CredentialOfferContext.class);
+        Assert.notNull(ctx, "Must not be null");
+        Assert.notNull(ctx.getCredentialConfigurations(), "Must not be null");
+        Assert.notNull(ctx.getValidatedPreAuthorizedCredentials(), "Must not be null");
+    }
+
+    @Test
+    public void testMissingMandatoryClaimVcSdJwt() throws Exception {
+        HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setQuery(
+                "{\"preAuthorizedCredentials\":{\"GeantIncubatorDiploma2\":[{\"path\":[\"credentialSubject\",\"givenName\"],\"value\":\"Gemma\"},{\"path\":[\"credentialSubject\",\"familyName\"],\"value\":\"Erasmus\"}]}}");
+        profileRequestCtx.getInboundMessageContext().setMessage(CredentialOfferRequest.parse(httpRequest));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
+    }
+
+    @Test
+    public void testCodeFlowSuccessVcSdJwt() throws Exception {
+        HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setQuery("{\"codeCredentials\":[\"GeantIncubatorDiploma2\"]}");
+        profileRequestCtx.getInboundMessageContext().setMessage(CredentialOfferRequest.parse(httpRequest));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        CredentialOfferContext ctx = profileRequestCtx.getInboundMessageContext()
+                .getSubcontext(CredentialOfferContext.class);
+        Assert.notNull(ctx, "Must not be null");
+        Assert.notNull(ctx.getValidatedCodeCredentials(), "Must not be null");
+    }
+
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/test/resources/conf/verifiable-credentials.json b/openid-vci-impl/src/test/resources/conf/verifiable-credentials.json
index 1d57042..64f5740 100644
--- a/openid-vci-impl/src/test/resources/conf/verifiable-credentials.json
+++ b/openid-vci-impl/src/test/resources/conf/verifiable-credentials.json
@@ -12,48 +12,50 @@
              ]
           }
         },
-        "display": [
-          {
-            "name": "GeantIncubatorCredential",
-            "description": "Card Credential",
-            "background_color": "rgba(0, 0, 0, 0.2)",
-            "text_color": "#FBFBFB",
-            "logo": {
-              "url": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
-              "alt_text": "Geant Incubator logo"
-            }
-          },
-          {
-            "locale": "en-EN",
-            "name": "GeantIncubatorCredential",
-            "description": "Geant Incubator statement",
-            "background_color": "rgba(0, 0, 0, 0.2)",
-            "text_color": "#FBFBFB",
-            "logo": {
-              "url": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
-              "alt_text": "Geant Incubator logo"
-            }
-          },
-          {
-            "locale": "fi-FI",
-            "name": "GeantIncubatorCredential",
-            "description": "Geant Incubator lausuma",
-            "background_color": "rgba(0, 0, 0, 0.2)",
-            "text_color": "#FBFBFB",
-            "logo": {
-              "url": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
-              "alt_text": "Geant Incubator logo"
-            }
-          }
-        ],
         "vct": "GeantIncubatorDiploma",
-        "claims": [
-          {"path": ["mail"],"display":[{"name":"E-mail","locale":"en-EN"}]},
-          {"path": ["eppn"],"display":[{"name":"Principal name","locale":"en-EN"}]},
-          {"path": ["givenName"],"display":[{"name":"CN","locale":"en-EN"}]},
-          {"path": ["familyName"],"display":[{"name":"SN","locale":"en-EN"}]},
-          {"path": ["affiliation"],"display":[{"name":"Affiliation","locale":"en-EN"}]}
-        ]
+        "credential_metadata": {
+          "display": [
+            {
+              "name": "GeantIncubatorCredential",
+              "description": "Card Credential",
+              "background_color": "rgba(0, 0, 0, 0.2)",
+              "text_color": "#FBFBFB",
+              "logo": {
+                "uri": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
+                "alt_text": "Geant Incubator logo"
+              }
+            },
+            {
+              "locale": "en-EN",
+              "name": "GeantIncubatorCredential",
+              "description": "Geant Incubator statement",
+              "background_color": "rgba(0, 0, 0, 0.2)",
+              "text_color": "#FBFBFB",
+              "logo": {
+                "uri": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
+                "alt_text": "Geant Incubator logo"
+              }
+            },
+            {
+              "locale": "fi-FI",
+              "name": "GeantIncubatorCredential",
+              "description": "Geant Incubator lausuma",
+              "background_color": "rgba(0, 0, 0, 0.2)",
+              "text_color": "#FBFBFB",
+              "logo": {
+                "uri": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
+                "alt_text": "Geant Incubator logo"
+              }
+            }
+          ],
+          "claims": [
+            {"path": ["mail"], "mandatory": true, "display":[{"name":"E-mail","locale":"en-EN"}]},
+            {"path": ["eppn"], "display":[{"name":"Principal name","locale":"en-EN"}]},
+            {"path": ["givenName"], "display":[{"name":"CN","locale":"en-EN"}]},
+            {"path": ["familyName"], "display":[{"name":"SN","locale":"en-EN"}]},
+            {"path": ["affiliation"], "display":[{"name":"Affiliation","locale":"en-EN"}]}
+          ]
+        }
       },
       "GeantIncubatorDiploma2": {
         "format": "vc+sd-jwt",
@@ -67,48 +69,54 @@
              ]
           }
         },
-        "display": [
-          {
-            "name": "GeantIncubatorCredential2",
-            "description": "Card Credential",
-            "background_color": "rgba(0, 0, 0, 0.2)",
-            "text_color": "#FBFBFB",
-            "logo": {
-              "url": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
-              "alt_text": "Geant Incubator logo"
-            }
-          },
-          {
-            "locale": "en-EN",
-            "name": "GeantIncubatorCredential2",
-            "description": "Geant Incubator statement",
-            "background_color": "rgba(0, 0, 0, 0.2)",
-            "text_color": "#FBFBFB",
-            "logo": {
-              "url": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
-              "alt_text": "Geant Incubator logo"
-            }
-          },
-          {
-            "locale": "fi-FI",
-            "name": "GeantIncubatorCredential2",
-            "description": "Geant Incubator lausuma",
-            "background_color": "rgba(0, 0, 0, 0.2)",
-            "text_color": "#FBFBFB",
-            "logo": {
-              "url": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
-              "alt_text": "Geant Incubator logo"
+        "credential_definition": {
+          "@context": ["https://www.w3.org/2018/credentials/v1"],
+          "type": ["VerifiableCredential", "GeantIncubatorDiploma2"],
+          "issuer": "https://issuer.example.org"
+        },
+        "credential_metadata": {
+          "display": [
+            {
+              "name": "GeantIncubatorCredential2",
+              "description": "Card Credential",
+              "background_color": "rgba(0, 0, 0, 0.2)",
+              "text_color": "#FBFBFB",
+              "logo": {
+                "uri": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
+                "alt_text": "Geant Incubator logo"
+              }
+            },
+            {
+              "locale": "en-EN",
+              "name": "GeantIncubatorCredential2",
+              "description": "Geant Incubator statement",
+              "background_color": "rgba(0, 0, 0, 0.2)",
+              "text_color": "#FBFBFB",
+              "logo": {
+                "uri": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
+                "alt_text": "Geant Incubator logo"
+              }
+            },
+            {
+              "locale": "fi-FI",
+              "name": "GeantIncubatorCredential2",
+              "description": "Geant Incubator lausuma",
+              "background_color": "rgba(0, 0, 0, 0.2)",
+              "text_color": "#FBFBFB",
+              "logo": {
+                "uri": "https://resources.geant.org/wp-content/uploads/2022/02/GEANT_logo_lo_res.jpg",
+                "alt_text": "Geant Incubator logo"
+              }
             }
-          }
-        ],
-        "vct": "GeantIncubatorDiploma2",
-        "claims": [
-          {"path": ["mail"],"display":[{"name":"E-mail","locale":"en-EN"}]},
-          {"path": ["eppn"],"display":[{"name":"Principal name","locale":"en-EN"}]},
-          {"path": ["givenName"],"display":[{"name":"CN","locale":"en-EN"}]},
-          {"path": ["familyName"],"display":[{"name":"SN","locale":"en-EN"}]},
-          {"path": ["affiliation"],"display":[{"name":"Affiliation","locale":"en-EN"}]}
-        ]
+          ],
+          "claims": [
+            {"path": ["credentialSubject", "mail"], "mandatory": true, "display":[{"name":"E-mail","locale":"en-EN"}]},
+            {"path": ["credentialSubject", "eppn"], "display":[{"name":"Principal name","locale":"en-EN"}]},
+            {"path": ["credentialSubject", "givenName"], "display":[{"name":"CN","locale":"en-EN"}]},
+            {"path": ["credentialSubject", "familyName"], "display":[{"name":"SN","locale":"en-EN"}]},
+            {"path": ["credentialSubject", "affiliation"], "display":[{"name":"Affiliation","locale":"en-EN"}]}
+          ]
+        }
       }
     }
-}
\ No newline at end of file
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list