[java-idp-plugin-vci] 01/01: Issue credentials not only by configuratin but also by identifier. Limiting issued credentials per scope in code flow. Fix to bind DPoP access tokens also in pre-auth. Replace deprecated Nimbus library calls.

Codeberg noreply at shibboleth.net
Mon Sep 28 11:00:42 UTC 2026


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

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

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

commit 99ac562ac720ad2e9ffeb5c15c880a608f05078e
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Mon Sep 28 13:59:48 2026 +0300

    Issue credentials not only by configuratin but also by identifier. Limiting issued credentials per scope in code flow. Fix to bind DPoP access tokens also in pre-auth. Replace deprecated Nimbus library calls.
---
 README.md                                          | 246 ++++++++++++++++++---
 ...eth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz | Bin 368363 -> 395645 bytes
 ...idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc |  22 +-
 .../openidvci/profile/OpenIDVCIEventIds.java       |   7 +
 .../profile/config/OpenIDVCIConfiguration.java     |  16 ++
 .../impl/CredentialOfferRequestDecoder.java        |   5 +-
 .../messaging/context/CredentialOfferContext.java  |  12 +-
 .../messaging/context/CredentialsContext.java      |  46 ++++
 .../openidvci/messaging/context/TokenContext.java  |  24 ++
 .../AuthorizationDetailsLookupFunction.java        |  59 ++++-
 .../messaging/impl/CredentialDataset.java          | 244 ++++++++++++++++++++
 .../messaging/impl/CredentialOfferRequest.java     |  45 ++--
 .../impl/CredentialOfferSuccessResponse.java       |   7 +-
 .../impl/OpenIDVCIAuthorizationDetail.java         |   9 +
 .../impl/OpenIDVCICredentialsRequest.java          |  14 +-
 .../impl/AbstractOpenIDVCIConfiguration.java       |  36 +++
 .../profile/impl/FilterPotentialCredentials.java   | 146 ++++++++++++
 .../SetAuthorizationDetailsToResponseContext.java  | 168 ++++++++------
 .../SetDPoPProofThumbprintToResponseContext.java   |  76 +++++++
 .../plugin/openidvci/profile/impl/UnwrapGrant.java |   3 +-
 .../profile/impl/ValidateCredentialOffering.java   |  32 +--
 .../profile/impl/ValidateRequestedCredential.java  | 189 +++++++++++-----
 .../logic/CredentialDetailsLookupFunction.java     |   5 +-
 .../logic/OpenIDVCITokenManipulationStrategy.java  |  12 +-
 .../logic/PotentialCredentialLookupFunction.java   |  99 +++++++++
 .../impl/StatusListAssignmentsRequest.java         |   2 +-
 .../messaging/impl/StatusListRevokeRequest.java    |   2 +-
 .../openidvci/storage/CredentialOfferObject.java   |   8 +-
 .../openid/vci/credentials/credentials-beans.xml   |   2 +
 .../idp/flows/openid/vci/token/token-beans.xml     |   8 +
 .../idp/flows/openid/vci/token/token-flow.xml      |   5 +
 .../AuthorizationDetailsLookupFunctionTest.java    | 117 ++++++++++
 .../messaging/impl/CredentialDatasetTest.java      | 129 +++++++++++
 .../messaging/impl/CredentialOfferRequestTest.java |  14 +-
 .../impl/OpenIDVCICredentialsRequestTest.java      |   6 +-
 .../messaging/impl/OpenIDVCITokenRequestTest.java  |   4 +-
 .../impl/FilterPotentialCredentialsTest.java       | 116 ++++++++++
 .../openidvci/profile/impl/ParseProofTest.java     |  16 +-
 ...tAuthorizationDetailsToResponseContextTest.java | 126 +++++++++++
 ...etDPoPProofThumbprintToResponseContextTest.java |  96 ++++++++
 .../impl/ValidateCredentialOfferingTest.java       |  45 +++-
 .../impl/ValidateRequestedCredentialTest.java      | 226 +++++++++++++++++++
 .../storage/CredentialOfferCacheTest.java          |   2 +-
 .../storage/CredentialOfferObjectTest.java         |   4 +-
 .../conf/verifiable-credentials-scoped.json        |  24 ++
 45 files changed, 2212 insertions(+), 262 deletions(-)

diff --git a/README.md b/README.md
index 3d12a8a..30ee58e 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,9 @@ limitations, **No** is not built and **No, by design** is left out on purpose.
 
 ### [OpenID for Verifiable Credential Issuance 1.0](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
 
+This is the base. The profile tables below list only what the profile requires in addition to it.
+Endpoints, metadata and error responses the specification mandates are not listed.
+
 | Feature | Status |
 |---|---|
 | Pre-Authorized Code Flow | Yes |
@@ -52,25 +55,21 @@ limitations, **No** is not built and **No, by design** is left out on purpose.
 | Authorization request with `scope` | Yes |
 | Authorization request with `authorization_details` | Partial |
 | PAR and PKCE | Yes |
-| Token endpoint, both grants | Yes |
 | Refresh tokens | No |
-| Client authentication | Yes |
 | DPoP | Yes |
+| Wallet Attestation, `attest_jwt_client_auth` | Yes |
 | Nonce endpoint | Yes |
 | Credential request by `credential_configuration_id` or `credential_identifier` | Yes |
 | Proof type `jwt` | Yes |
-| Key attestation in a `jwt` proof | Yes |
+| Key attestation in a `jwt` proof, `key_attestations_required` | Yes |
 | Proof types `di_vp` and `attestation` | No |
 | Batch issuance | Yes |
 | Credential Response encryption | No |
 | Credential Request encryption | No |
 | Deferred Credential Endpoint | No |
 | Notification Endpoint | No |
-| Credential and token error responses | Yes |
-| Credential Issuer metadata | Yes |
 | Signed Credential Issuer metadata | Yes |
 | Localised metadata by `Accept-Language` | No |
-| Wallet Attestation, `attest_jwt_client_auth` | Yes |
 | Format `dc+sd-jwt` | Yes |
 | Format `vc+sd-jwt` | Yes |
 | Formats `jwt_vc_json`, `jwt_vc_json-ld`, `ldp_vc`, `mso_mdoc` | No |
@@ -79,23 +78,16 @@ limitations, **No** is not built and **No, by design** is left out on purpose.
 
 ### [Decentralized Identity Interop Profile 5](https://fidescommunity.github.io/DIIP/)
 
+Requirements in addition to OpenID for Verifiable Credential Issuance 1.0 above.
+
 | Requirement | Status |
 |---|---|
-| SD-JWT VC | Yes |
 | W3C VCDM 2.0 secured with SD-JWT | Partial |
 | ES256 | Yes |
 | Issuer identifier `did:jwk` | Yes |
 | Issuer identifier `did:web` | Yes |
-| Holder identifier `did:jwk` | Yes |
-| Holder identifier `did:web` | No |
-| Pre-Authorized Code Flow and Authorization Code Flow | Yes |
-| `tx_code` | Yes |
-| PKCE `S256` and PAR | Yes |
 | Issuer initiated, same device and cross device | Yes |
-| Immediate issuance | Yes |
-| `scope` | Yes |
-| `authorization_details` | Partial |
-| Proof type `jwt` | Yes |
+| PKCE `S256` | Yes |
 | `cnf` as DID URL | Yes |
 | Validity, `nbf` `exp` `validFrom` `validUntil` | Yes |
 | Token Status List | Yes |
@@ -103,25 +95,22 @@ limitations, **No** is not built and **No, by design** is left out on purpose.
 
 ### [OpenID4VC High Assurance Interoperability Profile 1.0](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html)
 
-| Feature | Status |
+Requirements in addition to OpenID for Verifiable Credential Issuance 1.0 above.
+
+| Requirement | Status |
 |---|---|
-| `dc+sd-jwt` | Yes |
 | Issuer key as `x5c` chain in the credential | Yes |
 | `x5c` in Status List Token | Yes |
 | `x5c` in signed Credential Issuer metadata | Yes |
 | ES256 | Yes |
-| Authorization Code Flow with PAR and PKCE `S256` | Yes |
+| PAR and PKCE `S256` required | Yes |
 | `iss` in authorization response, RFC 9207 | Yes |
-| DPoP | Yes |
-| Wallet Attestation at PAR and token endpoint | Yes |
-| Key attestation, `key_attestations_required` | Yes |
-| Proof type `attestation` | No |
+| DPoP required | Yes |
+| Wallet Attestation required at PAR and token endpoint | Yes |
 | Holder key in `cnf` `jwk` member | Partial |
-| Pre-Authorized Code Flow | Yes |
 | Same device and cross device offers | Partial |
 | Batch issuance advertised by `batch_credential_issuance` | Yes |
 | Unique, unpredictable status list index | Yes |
-| Refresh tokens, RECOMMENDED | No |
 
 ## Plugin Installation
 
@@ -828,6 +817,14 @@ And you release it to the wallet in *conf/attribute-filter.xml*. This flow reads
 
 </details>
 
+The token endpoint of the code flow keeps only the credentials the wallet was authorized for: a
+configuration whose `scope` in the
+[Credential Issuer metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p) is in the granted scope, see
+[Using scope Parameter](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-scope-parameter-to-re), or one the wallet asked with
+[`authorization_details`](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-authorization-details). Everything else released by the filter is dropped from
+the access token. The filter can be narrower than that, for example per scope or per client, but it
+cannot widen it.
+
 #### Status List
 
 **File(s):** *conf/relying-party.xml*, *conf/openid-vci.properties*
@@ -1679,6 +1676,202 @@ is another key than the one of the credential.
 The whole *conf/relying-party.xml* is in [Relying party configuration](#relying-party-configuration)
 and the whole *etc/jetty-rewrite-rules.xml* in [Discovery documents](#discovery-documents).
 
+## Requesting a credential by configuration or by identifier
+
+**File(s):** *metadata/verifiable-credentials.json*, *metadata/oidc-client.json*, *conf/relying-party.xml*
+**Format:** JSON, Native Spring
+
+A wallet names the credential it wants in the
+[Credential Request](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request) in one of two ways, see
+[Section 3.3.4](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-identifying-credentials-bei) and the [Terminology](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#terminology):
+
+- `credential_configuration_id`, a key of `credential_configurations_supported` in your
+  [Credential Issuer metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p). It names the kind of
+  credential.
+- `credential_identifier`, one of the `credential_identifiers` this issuer returned in the
+  [token response](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response). It names one Credential Dataset of a
+  configuration.
+
+The wallet uses `credential_identifier` when the token response has `credential_identifiers`, and
+`credential_configuration_id` otherwise. By default nothing needs configuring, the simple path of
+[First installation](#first-installation-by-example) uses `credential_configuration_id` only.
+
+What you control:
+
+- **What a code flow wallet may ask for.** The wallet asks a credential by
+  [`scope`](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-scope-parameter-to-re) or by
+  [`authorization_details`](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-authorization-details). For `scope`, give the
+  configuration a `scope` in *metadata/verifiable-credentials.json* and allow it for the client in
+  *metadata/oidc-client.json*. The access token covers only the configurations whose `scope` was
+  granted and those the wallet named in `authorization_details`, whatever else the attribute filter
+  released. A configuration without `scope` can be asked only with `authorization_details`.
+- **When identifiers are issued.** The token response has `credential_identifiers` for a
+  configuration the wallet asked with `authorization_details`, in the authorization request, also a
+  pushed one, or in the token request, and for an authorized configuration that has several
+  credentials for the user, see
+  [Several credentials of one configuration](#several-credentials-of-one-configuration). Otherwise
+  it has none, and most wallets, which use `credential_configuration_id` only, work as they are.
+- **Wallets that ignore the identifiers.** Once identifiers were issued, the credential endpoint
+  accepts only those and refuses `credential_configuration_id`, as
+  [Section 8.2](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request) requires. Some wallets ask with
+  `authorization_details` and still send `credential_configuration_id`. To accept that, set
+  `allowCredentialConfigurationIdWithIdentifiers` on the credentials profile of those wallets. It
+  does not help with a configuration of several credentials, there the configuration id cannot tell
+  which one.
+
+```xml
+<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller/wallet">
+  <property name="profileConfigurations">
+      <list>
+          <bean parent="OpenID.VCI.Token"
+                p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+                p:tokenEndpointAuthMethods="#{{'none'}}" />
+          <bean parent="OpenID.VCI.Credentials" p:allowCredentialConfigurationIdWithIdentifiers="true" />
+      </list>
+  </property>
+</bean>
+```
+
+## Several credentials of one configuration
+
+**File(s):** *metadata/verifiable-credentials.json*, *conf/attribute-resolver.xml*
+**Format:** Native Spring, JSON
+
+Check that your wallets support this before you configure several datasets. The wallet has to read
+`credential_identifiers` from the token response and request each credential by
+`credential_identifier`. A wallet that requests by `credential_configuration_id` only gets
+`invalid_credential_request` and no credential. Support for this varies between wallets and
+wallet versions. In our own tests the EUDI reference wallet libraries handled it and some other
+wallets did not.
+
+One Credential Configuration may be issued more than once for the same user. A user with a
+Master of Science in Computer Science and another in Mathematics gets two credentials of one
+`UniversityDegree` configuration:
+
+<details>
+<summary>Example snippet of metadata/verifiable-credentials.json</summary>
+
+```json
+{
+  "credential_configurations_supported": {
+    "UniversityDegree": {
+      "format": "dc+sd-jwt",
+      "vct": "UniversityDegree",
+      "scope": "UniversityDegree",
+      "cryptographic_binding_methods_supported": ["did:jwk"],
+      "proof_types_supported": {
+        "jwt": {
+          "proof_signing_alg_values_supported": ["ES256"]
+        }
+      },
+      "credential_metadata": {
+        "display": [
+          { "name": "University Degree", "locale": "en" }
+        ],
+        "claims": [
+          { "path": ["degree"],     "display": [{ "name": "Degree",      "locale": "en" }] },
+          { "path": ["field"],      "display": [{ "name": "Field",       "locale": "en" }] },
+          { "path": ["givenName"],  "display": [{ "name": "Given names", "locale": "en" }] },
+          { "path": ["familyName"], "display": [{ "name": "Family name", "locale": "en" }] }
+        ]
+      }
+    }
+  }
+}
+```
+
+</details>
+
+The metadata has `UniversityDegree` once, the plugin issues it twice with different claim values.
+
+For a single credential you give the claims as one list of `path` and `value`. For two credentials
+of one configuration you give two lists and put an id next to each, the `dataset_id`, so the
+plugin can tell them apart. You choose the ids, here `cs` and `math`. The specification calls the
+claims of one credential a Credential Dataset, see
+[Terminology](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#terminology).
+How the datasets reach the plugin is shown below for the pre-authorized code flow and the
+authorization code flow.
+
+The token response has one identifier per dataset in `credential_identifiers`, see
+[Successful Token Response](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response).
+The identifier is the configuration id and the `dataset_id` joined with `#`:
+
+```json
+"authorization_details": [{
+  "type": "openid_credential",
+  "credential_configuration_id": "UniversityDegree",
+  "credential_identifiers": ["UniversityDegree#cs", "UniversityDegree#math"]
+}]
+```
+
+The wallet then asks for each with `credential_identifier` in the
+[Credential Request](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request)
+and ends up with two `UniversityDegree` credentials, Computer Science and Mathematics. The token
+response has the identifiers also when the wallet did not ask with `authorization_details`, as
+they are the only way to address one of the datasets.
+
+### Pre-authorized code flow
+
+The value of a credential configuration id in the offer request is a list of datasets instead of
+the claims:
+
+```json
+{
+  "preAuthorizedCredentials": {
+    "UniversityDegree": [
+      {
+        "dataset_id": "cs",
+        "claims": [
+          { "path": ["degree"],     "value": "Master of Science" },
+          { "path": ["field"],      "value": "Computer Science" },
+          { "path": ["givenName"],  "value": "Gemma" },
+          { "path": ["familyName"], "value": "Erasmus" }
+        ]
+      },
+      {
+        "dataset_id": "math",
+        "claims": [
+          { "path": ["degree"],     "value": "Master of Science" },
+          { "path": ["field"],      "value": "Mathematics" },
+          { "path": ["givenName"],  "value": "Gemma" },
+          { "path": ["familyName"], "value": "Erasmus" }
+        ]
+      }
+    ]
+  }
+}
+```
+
+Each dataset needs a `dataset_id` of its own. The offer lists `UniversityDegree` once in
+`credential_configuration_ids`.
+
+### Authorization code flow
+
+The attribute has one value per dataset, and the encoder puts them in an array:
+
+```xml
+<AttributeDefinition id="UniversityDegrees" xsi:type="ScriptedAttribute">
+  <InputAttributeDefinition ref="givenName" />
+  <InputAttributeDefinition ref="sn" />
+  <Script><![CDATA[
+      var degree = function(id, field) {
+          return '{"dataset_id":"' + id + '","claims":['
+              + '{"path":["degree"],"value":"Master of Science"},'
+              + '{"path":["field"],"value":"' + field + '"},'
+              + '{"path":["givenName"],"value":"' + givenName.getValues().get(0) + '"},'
+              + '{"path":["familyName"],"value":"' + sn.getValues().get(0) + '"}'
+              + ']}';
+      };
+      UniversityDegrees.addValue(degree('cs', 'Computer Science'));
+      UniversityDegrees.addValue(degree('math', 'Mathematics'));
+  ]]></Script>
+  <AttributeEncoder xsi:type="oidc:OIDCString" name="UniversityDegree" asArray="true" />
+</AttributeDefinition>
+```
+
+Without `asArray="true"` the encoder joins the values into one string and they are not readable
+anymore. An attribute of one value needs neither the `dataset_id` nor `asArray`.
+
 ## Reference
 
 ### Profile configuration settings
@@ -1719,7 +1912,7 @@ Request, a `POST` of `application/json`:
 
 | Name | Type | Description |
 |---|---|---|
-| `preAuthorizedCredentials` | Object | Required. Keyed with credential configuration id, each an array of `{"path": [...], "value": ...}`. |
+| `preAuthorizedCredentials` | Object | Required. Keyed with credential configuration id, each an array of `{"path": [...], "value": ...}`, or a list of datasets `{"dataset_id": ..., "claims": [...]}`. See [Several credentials of one configuration](#several-credentials-of-one-configuration). |
 | `txCode` | Object | `value`, and hints `input_mode` (`numeric` or `text`), `length` and `description`. `value` is not in the offer. |
 | `client_id` | String | Only where unauthenticated callers are accepted. |
 
@@ -1752,6 +1945,7 @@ Endpoint `/idp/profile/openid/vci/credentials`, profile ID `http://geant.org/ns/
 | `proofNonceGenerator` | Function<ProfileRequestContext,String> | **openidvci.DefaultOpenIDVCINonceGenerator** | Nonce a key proof has to carry. Same generator as in **OpenID.VCI.Nonce**. `#{null}` in both for no nonce. |
 | `batchSize` | Integer | `2` | Largest number of key proofs in one request. Not read when the Credential Issuer metadata has `batch_credential_issuance`, its `batch_size` is used. Property `openidvci.batchSize`. |
 | `keyAttestationClaimsValidator` | ClaimsValidator | bean **openidvci.KeyAttestationClaimsValidator** if defined | Validation of key attestation claims, `nonce` among them. |
+| `allowCredentialConfigurationIdWithIdentifiers` | Boolean | `false` | Accept `credential_configuration_id` in a Credential Request although credential identifiers were issued. See [Requesting a credential by configuration or by identifier](#requesting-a-credential-by-configuration-or-by-identifier). |
 | `dpopProofNonceGenerator` | Function | | Of the OP plugin. `#{null}` for no DPoP nonce. |
 | `inboundInterceptorFlows` | List<String> | | Interceptor flows run after the request is decoded, before authentication. |
 | `outboundInterceptorFlows` | List<String> | | Interceptor flows run before the response is sent, also for an error response. |
diff --git a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz
index 4b265b8..859407b 100644
Binary files a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz and b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz differ
diff --git a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc
index c731386..cd90f78 100644
--- a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc
+++ b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc
@@ -1,14 +1,14 @@
 -----BEGIN PGP SIGNATURE-----
 
-iQHPBAABCAA5FiEEoCV/y8Vr4Y+djZGw+mRLGOJQ2wsFAmqzzxcbFIAAAAAABAAO
-bWFudTIsMi41KzEuMTIsMCwzAAoJEPpkSxjiUNsLsiEL/16tF0mVQuOOdoua1KN4
-EVWAnY/k2drQb0mRogNqcKWeWbiof81wjjTFBjonlXxH8dAMyUf2M1AQKLYWpkNC
-lSYLZ0yNSyOuCw3bPC2dFmMlxbMWQKuiaRqg2WiYpQngNlMObz8+mIOHSbqrM3OH
-NEU7RizpMP9XgHIDHQ6+E6vwp2BE2sIFlwwFyvxCP9sQcp24vUhD/VGbPhFL9Llq
-SMRByIRpz+TIsUWlZsnVb+1IqSIIVbmi6FzbRM7Jkx7NLjXWkPOchw7cVx7zeLYl
-XGh8MS/675nk31qBBFt5658sAjfGisuJNrN7CfZ17/egR/LkjzTkpsoR2RPGmSrl
-RsJfuMCuwP0phBSFAQ1O8aLNA4qlwfWqzYd+zNc6AUJTWdb4jZgFqn0Wf5l9gOav
-bay/S1bPdfO6GnJjhB4kE6qz4i/uVVs5Lv+XU7sSFKwHFiW6J8GNe5mJz0/ffkWN
-fqCsA83DDKnYXACmMW+l1cc/KmUr0Aban+z5LPj9uFSUOQ==
-=8Vwt
+iQHPBAABCAA5FiEEoCV/y8Vr4Y+djZGw+mRLGOJQ2wsFAmq6R+wbFIAAAAAABAAO
+bWFudTIsMi41KzEuMTIsMCwzAAoJEPpkSxjiUNsLw4IL/1X7bEzE4Bps1vJeSvnP
+6y1HM/timTodH2jgad1lfnqo4ybbvwUs1z0AJKyvNB13SddCf9rBD37dLW5jaGuf
+e+1/3/0Vbg3fx9JwsVjBpXNwuwGMpbNWXM05hwNE0celXewsqW3bP9q75HTKUQSK
+Hia4S6B5v5CzWalC2WTCSMofrTNlDc4L028rzh8kUd1WTp7C18f9j5sRryNbHbRy
+B+2WaAMPiU4H1sCK2Z2mVIdyI7xfh92TRDpISN8BBDyM/a93rb2VSNvtllzXyacP
+GwVRZvl8R+d/en27K42MYmchhFeVn4cOKPowBcwRsplqfyD7b7cMrpTVLlSEtFAX
+SjFCsHzDW/a95eG5h2zfL/KVW8JKCZkTTZE4yco10grINyiJyKc4V9Du8fnryTSV
+P/+ahTmBt/Jo1fbye7goI2mZJMx2bNvc9+EjTaTeQ8ee4hrOiRPLYpbfy/V/WpO1
+OjvJWVqEnSHcBGnCCdy4tQsNATfaV/GofOg02R/L4fxg+g==
+=2bbK
 -----END PGP SIGNATURE-----
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/OpenIDVCIEventIds.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/OpenIDVCIEventIds.java
index 41db26f..828094d 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/OpenIDVCIEventIds.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/OpenIDVCIEventIds.java
@@ -64,6 +64,13 @@ public final class OpenIDVCIEventIds {
     @NotEmpty
     public static final String NO_CREDENTIALS_FOR_REQUEST = "NoCredentialsForRequest";
 
+    /**
+     * ID of event returned if the credential identifier of the request was not issued.
+     */
+    @Nonnull
+    @NotEmpty
+    public static final String UNKNOWN_CREDENTIAL_IDENTIFIER = "UnknownCredentialIdentifier";
+
     /**
      * Parsing of credential failed.
      */
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/OpenIDVCIConfiguration.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/OpenIDVCIConfiguration.java
index e0ebaac..bc8b482 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/OpenIDVCIConfiguration.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/OpenIDVCIConfiguration.java
@@ -202,4 +202,20 @@ public interface OpenIDVCIConfiguration extends ConditionalProfileConfiguration
     Function<ProfileRequestContext, String> getProofNonceGenerator(
             @Nullable final ProfileRequestContext profileRequestContext);
 
+    /**
+     * Get whether a Credential Request may name a credential configuration id when credential identifiers were
+     * issued.
+     *
+     * <p>
+     * Defaults to false.
+     * </p>
+     *
+     * @param profileRequestContext profile request context
+     *
+     * @return whether a credential configuration id is accepted when credential identifiers were issued
+     */
+    @ConfigurationSetting(name = "allowCredentialConfigurationIdWithIdentifiers")
+    boolean isAllowCredentialConfigurationIdWithIdentifiers(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/CredentialOfferRequestDecoder.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/CredentialOfferRequestDecoder.java
index ef28f28..713e35c 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/CredentialOfferRequestDecoder.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/CredentialOfferRequestDecoder.java
@@ -17,12 +17,13 @@
 package org.geant.shibboleth.plugin.openidvci.decoding.impl;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequest;
-import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
 import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -61,7 +62,7 @@ public class CredentialOfferRequestDecoder extends BaseOpenIDVCIRequestDecoder<C
     @Override
     protected String getMessageToLog(final CredentialOfferRequest message) {
 
-        final Map<String, CredentialOfferRequestedCredential> request = message.getPreAuthorizedCredentials();
+        final Map<String, List<CredentialDataset>> request = message.getPreAuthorizedCredentials();
         String requestAsString = null;
         if (request != null) {
             try {
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialOfferContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialOfferContext.java
index 1717da6..2307d72 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialOfferContext.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialOfferContext.java
@@ -16,12 +16,13 @@
 
 package org.geant.shibboleth.plugin.openidvci.messaging.context;
 
+import java.util.List;
 import java.util.Map;
 
 import javax.annotation.Nullable;
 
 import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
-import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferTxCode;
 import org.opensaml.messaging.context.BaseContext;
 
@@ -37,7 +38,7 @@ public class CredentialOfferContext extends BaseContext {
 
     /** Credential offer request content in pre-auth flow. */
     @Nullable
-    private Map<String, CredentialOfferRequestedCredential> preAuthorizedCredentials;
+    private Map<String, List<CredentialDataset>> preAuthorizedCredentials;
 
     /** Transaction Code. */
     @Nullable
@@ -76,8 +77,7 @@ public class CredentialOfferContext extends BaseContext {
      * @param credentials offer request content in pre-auth flow
      */
     @Nullable
-    public void setValidatedPreAuthorizedCredentials(
-            @Nullable final Map<String, CredentialOfferRequestedCredential> credentials) {
+    public void setValidatedPreAuthorizedCredentials(@Nullable final Map<String, List<CredentialDataset>> credentials) {
         this.preAuthorizedCredentials = credentials;
     }
 
@@ -87,12 +87,10 @@ public class CredentialOfferContext extends BaseContext {
      * @return Credential offer request content in pre-auth flow
      */
     @Nullable
-    public Map<String, CredentialOfferRequestedCredential> getValidatedPreAuthorizedCredentials() {
+    public Map<String, List<CredentialDataset>> getValidatedPreAuthorizedCredentials() {
         return preAuthorizedCredentials;
     }
 
-
-
     /**
      * Get Transaction Code.
      * 
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
index 3e5777e..548c4fa 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
@@ -46,6 +46,14 @@ public class CredentialsContext extends BaseContext {
     @Nullable
     private String credentialIdentifier;
 
+    /** Credential configuration id of the final credential. */
+    @Nullable
+    private String credentialConfigurationId;
+
+    /** Dataset id of the final credential. */
+    @Nullable
+    private String datasetId;
+
     /** Validated proofs of wallet. */
     @Nullable
     private List<SignedJWT> proofs;
@@ -219,6 +227,44 @@ public class CredentialsContext extends BaseContext {
         this.credentialIdentifier = credentialIdentifier;
     }
 
+    /**
+     * Get credential configuration id of the final credential.
+     *
+     * @return credential configuration id of the final credential
+     */
+    @Nullable
+    public String getCredentialConfigurationId() {
+        return credentialConfigurationId;
+    }
+
+    /**
+     * Set credential configuration id of the final credential.
+     *
+     * @param id credential configuration id of the final credential
+     */
+    public void setCredentialConfigurationId(@Nullable final String id) {
+        credentialConfigurationId = id;
+    }
+
+    /**
+     * Get dataset id of the final credential.
+     *
+     * @return dataset id of the final credential, or null
+     */
+    @Nullable
+    public String getDatasetId() {
+        return datasetId;
+    }
+
+    /**
+     * Set dataset id of the final credential.
+     *
+     * @param id dataset id of the final credential
+     */
+    public void setDatasetId(@Nullable final String id) {
+        datasetId = id;
+    }
+
     /**
      * Get credential shell(s) per proof.
      * 
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/TokenContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/TokenContext.java
index c81d2f5..cf85d79 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/TokenContext.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/TokenContext.java
@@ -17,6 +17,7 @@
 package org.geant.shibboleth.plugin.openidvci.messaging.context;
 
 import java.util.List;
+import java.util.Map;
 
 import javax.annotation.Nullable;
 
@@ -50,6 +51,10 @@ public class TokenContext extends BaseContext {
     @Nullable
     private List<OpenIDVCIAuthorizationDetail> responseAuthorizationDetails;
 
+    /** Credential identifiers of the token response keyed with the credential dataset they identify. */
+    @Nullable
+    private Map<String, Object> issuedCredentialIdentifiers;
+
     /**
      * Get the Credential Offer being redeemed.
      *
@@ -125,5 +130,24 @@ public class TokenContext extends BaseContext {
         this.responseAuthorizationDetails = responseAuthorizationDetails;
     }
 
+    /**
+     * Get the credential identifiers of the token response keyed with the credential dataset they identify.
+     *
+     * @return issued credential identifiers, or null
+     */
+    @Nullable
+    public Map<String, Object> getIssuedCredentialIdentifiers() {
+        return issuedCredentialIdentifiers;
+    }
+
+    /**
+     * Set the credential identifiers of the token response keyed with the credential dataset they identify.
+     *
+     * @param identifiers issued credential identifiers
+     */
+    public void setIssuedCredentialIdentifiers(@Nullable final Map<String, Object> identifiers) {
+        issuedCredentialIdentifiers = identifiers;
+    }
+
 
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/navigate/AuthorizationDetailsLookupFunction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/navigate/AuthorizationDetailsLookupFunction.java
index 6f8ad27..d859b63 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/navigate/AuthorizationDetailsLookupFunction.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/navigate/AuthorizationDetailsLookupFunction.java
@@ -16,9 +16,11 @@
 
 package org.geant.shibboleth.plugin.openidvci.messaging.context.navigate;
 
+import java.text.ParseException;
 import java.util.List;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCIAuthorizationDetail;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCITokenRequest;
@@ -30,20 +32,25 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.JWT;
 import com.nimbusds.oauth2.sdk.AuthorizationRequest;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 
 /**
  * A function that returns list of {@link OpenIDVCIAuthorizationDetail} parsed
- * from from 1. {@link AuthorizationRequest}, 2. {@link OpenIDVCITokenRequest}
- * or from claim encoded to 3. authorization code grant.
- *
- * <p>TODO Locate details also in PAR and or Request Object case.</p>
+ * from from 1. {@link AuthorizationRequest} or its request object, 2.
+ * {@link OpenIDVCITokenRequest} or from claim encoded to 3. authorization code
+ * grant.
  */
 public class AuthorizationDetailsLookupFunction
         implements ContextDataLookupFunction<ProfileRequestContext, List<OpenIDVCIAuthorizationDetail>> {
 
+    /** Name of the authorization details parameter. */
+    @Nonnull
+    private static final String AUTHORIZATION_DETAILS = "authorization_details";
+
     /** Class logger. */
     @Nonnull
     private final Logger log = LoggerFactory.getLogger(AuthorizationDetailsLookupFunction.class);
@@ -57,6 +64,10 @@ public class AuthorizationDetailsLookupFunction
         }
         if (input.getInboundMessageContext().getMessage() instanceof AuthorizationRequest request) {
             try {
+                final JWT requestObject = lookupRequestObject(input);
+                if (requestObject != null) {
+                    return parse(requestObject);
+                }
                 return OpenIDVCIAuthorizationDetail.parse(request.toHTTPRequest());
             } catch (final JsonProcessingException e) {
                 log.warn("Failed parsing authorization_details", e);
@@ -91,4 +102,44 @@ public class AuthorizationDetailsLookupFunction
         return null;
     }
 // Checkstyle: CyclomaticComplexity|ReturnCount ON
+
+    /**
+     * Get the request object of the authorization request, pushed or by value.
+     *
+     * @param input profile request context
+     * @return request object, or null if there is none
+     */
+    @Nullable
+    private JWT lookupRequestObject(@Nonnull final ProfileRequestContext input) {
+        final MessageContext msgCtx = input.getOutboundMessageContext();
+        if (msgCtx == null) {
+            return null;
+        }
+        final OIDCAuthenticationResponseContext oidcCtx = msgCtx.getSubcontext(OIDCAuthenticationResponseContext.class);
+        return oidcCtx == null ? null : oidcCtx.getRequestObject();
+    }
+
+    /**
+     * Parse authorization details from a request object.
+     *
+     * @param requestObject request object
+     * @return authorization details, or null if the request object has none
+     * @throws JsonProcessingException if parsing fails
+     */
+    @Nullable
+    private List<OpenIDVCIAuthorizationDetail> parse(@Nonnull final JWT requestObject) throws JsonProcessingException {
+        final Object details;
+        try {
+            details = requestObject.getJWTClaimsSet().getClaim(AUTHORIZATION_DETAILS);
+        } catch (final ParseException e) {
+            log.warn("Failed parsing request object", e);
+            return null;
+        }
+        if (details == null) {
+            return null;
+        }
+        final String serialized = details instanceof String value ? value
+                : new ObjectMapper().writeValueAsString(details);
+        return serialized.isEmpty() ? null : OpenIDVCIAuthorizationDetail.parse(serialized);
+    }
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialDataset.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialDataset.java
new file mode 100644
index 0000000..bd97344
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialDataset.java
@@ -0,0 +1,244 @@
+/*
+ * 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.messaging.impl;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.ParseException;
+
+/**
+ * Credential Dataset, the claims of one credential of a credential
+ * configuration.
+ */
+ at JsonInclude(Include.NON_NULL)
+public class CredentialDataset {
+
+    /** Member name of the dataset id. */
+    @Nonnull
+    public static final String DATASET_ID = "dataset_id";
+
+    /** Member name of the claims. */
+    @Nonnull
+    public static final String CLAIMS = "claims";
+
+    /** Member name of the requested claims of a requested credential. */
+    @Nonnull
+    private static final String REQUESTED_CREDENTIAL = "requestedCredential";
+
+    /** Dataset id. */
+    @Nullable
+    private final String datasetId;
+
+    /** Claims of the dataset. */
+    @Nonnull
+    private final CredentialOfferRequestedCredential claims;
+
+    /**
+     * Constructor.
+     *
+     * @param id      dataset id, may be null for the only dataset of a credential
+     *                configuration
+     * @param content claims of the dataset
+     */
+    public CredentialDataset(@Nullable final String id, @Nonnull final CredentialOfferRequestedCredential content) {
+        datasetId = id;
+        claims = content;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param id              dataset id
+     * @param requestedClaims claims of the dataset
+     */
+    @JsonCreator
+    private CredentialDataset(@JsonProperty(DATASET_ID) @Nullable final String id,
+            @JsonProperty(CLAIMS) @Nonnull final List<CredentialOfferRequestedClaim> requestedClaims) {
+        this(id, new CredentialOfferRequestedCredential(requestedClaims));
+    }
+
+    /**
+     * Get dataset id.
+     *
+     * @return dataset id, or null
+     */
+    @JsonProperty(DATASET_ID)
+    @Nullable
+    public String getDatasetId() {
+        return datasetId;
+    }
+
+    /**
+     * Get content of the dataset.
+     *
+     * @return content of the dataset
+     */
+    @JsonIgnore
+    @Nonnull
+    public CredentialOfferRequestedCredential getContent() {
+        return claims;
+    }
+
+    /**
+     * Get requested claims of the dataset.
+     *
+     * @return requested claims of the dataset
+     */
+    @JsonProperty(CLAIMS)
+    @Nonnull
+    public List<CredentialOfferRequestedClaim> getRequestedClaims() {
+        return claims.getRequestedCredential();
+    }
+
+    /**
+     * Get the credential identifier of a dataset of a credential configuration.
+     *
+     * @param configurationId credential configuration id
+     * @param dataset         dataset
+     * @return credential identifier
+     */
+    @Nonnull
+    public static String credentialIdentifier(@Nonnull final String configurationId,
+            @Nonnull final CredentialDataset dataset) {
+        return dataset.getDatasetId() == null ? configurationId : configurationId + "#" + dataset.getDatasetId();
+    }
+
+    /**
+     * Serialize datasets to json.
+     *
+     * @param datasets datasets to serialize
+     * @return datasets as a json string
+     * @throws JsonProcessingException if serialization fails
+     */
+    @Nonnull
+    public static String serialize(@Nonnull final List<CredentialDataset> datasets) throws JsonProcessingException {
+        return new ObjectMapper().writeValueAsString(datasets);
+    }
+
+    /**
+     * Parse the datasets of a credential configuration.
+     *
+     * @param value claims of one dataset, or a list of datasets, as parsed or as
+     *              json
+     * @return datasets
+     * @throws ParseException if the value cannot be parsed or datasets are not
+     *                        uniquely identified
+     */
+    @Nonnull
+    public static List<CredentialDataset> parse(@Nullable final Object value) throws ParseException {
+        final Object parsed = value instanceof String json ? readJson(json) : value;
+        final List<CredentialDataset> datasets = new ArrayList<>();
+        try {
+            if (parsed instanceof List<?> values && !values.isEmpty()
+                    && values.stream().allMatch(CredentialDataset::isDataset)) {
+                for (final Object dataset : values) {
+                    datasets.add(parseDataset(dataset));
+                }
+            } else {
+                datasets.add(parseDataset(parsed));
+            }
+        } catch (final IllegalArgumentException e) {
+            throw new ParseException("Invalid credential dataset: " + e.getMessage());
+        }
+        validate(datasets);
+        return datasets;
+    }
+
+    /**
+     * Get whether a value is a dataset of a list of datasets.
+     *
+     * @param value value to test
+     * @return whether the value is a dataset
+     */
+    private static boolean isDataset(@Nullable final Object value) {
+        return value instanceof String || value instanceof Map<?, ?> map && map.containsKey(CLAIMS);
+    }
+
+    /**
+     * Parse one dataset.
+     *
+     * @param value dataset, or the claims of a dataset, as parsed or as json
+     * @return dataset
+     * @throws ParseException if the value cannot be parsed
+     */
+    @Nonnull
+    private static CredentialDataset parseDataset(@Nullable final Object value) throws ParseException {
+        final Object parsed = value instanceof String json ? readJson(json) : value;
+        if (parsed instanceof Map<?, ?> map && map.containsKey(CLAIMS)) {
+            final Object id = map.get(DATASET_ID);
+            if (id != null && !(id instanceof String)) {
+                throw new ParseException("Invalid credential dataset: " + DATASET_ID + " must be a string");
+            }
+            return new CredentialDataset((String) id, CredentialOfferRequestedCredential.parse(map.get(CLAIMS)));
+        }
+        if (parsed instanceof Map<?, ?> map && map.containsKey(REQUESTED_CREDENTIAL)) {
+            return new CredentialDataset(null, CredentialOfferRequestedCredential.parse(map.get(REQUESTED_CREDENTIAL)));
+        }
+        return new CredentialDataset(null, CredentialOfferRequestedCredential.parse(parsed));
+    }
+
+    /**
+     * Validate datasets of a credential configuration are uniquely identified.
+     *
+     * @param datasets datasets to validate
+     * @throws ParseException if datasets are not uniquely identified
+     */
+    private static void validate(@Nonnull final List<CredentialDataset> datasets) throws ParseException {
+        if (datasets.size() < 2) {
+            return;
+        }
+        final Set<String> ids = new HashSet<>();
+        for (final CredentialDataset dataset : datasets) {
+            if (dataset.getDatasetId() == null || dataset.getDatasetId().isEmpty()
+                    || !ids.add(dataset.getDatasetId())) {
+                throw new ParseException(
+                        "Invalid credential datasets: each of several datasets must have an unique " + DATASET_ID);
+            }
+        }
+    }
+
+    /**
+     * Read json.
+     *
+     * @param json json to read
+     * @return parsed json
+     * @throws ParseException if the json cannot be parsed
+     */
+    @Nullable
+    private static Object readJson(@Nonnull final String json) throws ParseException {
+        try {
+            return new ObjectMapper().readValue(json, Object.class);
+        } catch (final JsonProcessingException e) {
+            throw new ParseException("Invalid credential dataset: " + e.getMessage());
+        }
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequest.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequest.java
index 257fbb8..6364cb3 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequest.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequest.java
@@ -19,6 +19,7 @@ package org.geant.shibboleth.plugin.openidvci.messaging.impl;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
@@ -50,7 +51,7 @@ public class CredentialOfferRequest extends AbstractOptionallyIdentifiedRequest
 
     /** Credential offer request content in pre-auth flow. */
     @Nullable
-    private final Map<String, CredentialOfferRequestedCredential> preAuthorizedCredentials;
+    private final Map<String, List<CredentialDataset>> preAuthorizedCredentials;
 
     /** Transaction Code. */
     @Nullable
@@ -59,16 +60,19 @@ public class CredentialOfferRequest extends AbstractOptionallyIdentifiedRequest
     /**
      * Constructor.
      * 
-     * @param uri        The URI of the endpoint (HTTP or HTTPS) for which the
-     *                   request is intended, {@code null} if not specified (if, for
-     *                   example, the {@link #toHTTPRequest()} method will not be
-     *                   used).
-     * @param clientAuth The client authentication, {@code null} if none.
-     * @param preAuthorizedCredentials Credentials offered in the pre-authorized code flow.
+     * @param uri                      The URI of the endpoint (HTTP or HTTPS) for
+     *                                 which the request is intended, {@code null}
+     *                                 if not specified (if, for example, the
+     *                                 {@link #toHTTPRequest()} method will not be
+     *                                 used).
+     * @param clientAuth               The client authentication, {@code null} if
+     *                                 none.
+     * @param preAuthorizedCredentials Credentials offered in the pre-authorized
+     *                                 code flow.
      * @param credentialOfferTxCode    Transaction Code the wallet must present.
      */
     public CredentialOfferRequest(@Nullable final URI uri, @Nullable final ClientAuthentication clientAuth,
-            @Nullable final Map<String, CredentialOfferRequestedCredential> preAuthorizedCredentials,
+            @Nullable final Map<String, List<CredentialDataset>> preAuthorizedCredentials,
             @Nullable final CredentialOfferTxCode credentialOfferTxCode) {
         super(uri, clientAuth);
         this.preAuthorizedCredentials = preAuthorizedCredentials;
@@ -78,15 +82,19 @@ public class CredentialOfferRequest extends AbstractOptionallyIdentifiedRequest
     /**
      * Constructor.
      * 
-     * @param uri      The URI of the endpoint (HTTP or HTTPS) for which the request
-     *                 is intended, {@code null} if not specified (if, for example,
-     *                 the {@link #toHTTPRequest()} method will not be used).
-     * @param clientID The client identifier, {@code null} if not specified.
-     * @param preAuthorizedCredentials Credentials offered in the pre-authorized code flow.
+     * @param uri                      The URI of the endpoint (HTTP or HTTPS) for
+     *                                 which the request is intended, {@code null}
+     *                                 if not specified (if, for example, the
+     *                                 {@link #toHTTPRequest()} method will not be
+     *                                 used).
+     * @param clientID                 The client identifier, {@code null} if not
+     *                                 specified.
+     * @param preAuthorizedCredentials Credentials offered in the pre-authorized
+     *                                 code flow.
      * @param credentialOfferTxCode    Transaction Code the wallet must present.
      */
     public CredentialOfferRequest(@Nullable final URI uri, @Nullable final ClientID clientID,
-            @Nullable final Map<String, CredentialOfferRequestedCredential> preAuthorizedCredentials,
+            @Nullable final Map<String, List<CredentialDataset>> preAuthorizedCredentials,
             @Nullable final CredentialOfferTxCode credentialOfferTxCode) {
         super(uri, clientID);
         this.preAuthorizedCredentials = preAuthorizedCredentials;
@@ -98,7 +106,7 @@ public class CredentialOfferRequest extends AbstractOptionallyIdentifiedRequest
      * 
      * @return credential offer request content in pre-auth flow.
      */
-    public Map<String, CredentialOfferRequestedCredential> getPreAuthorizedCredentials() {
+    public Map<String, List<CredentialDataset>> getPreAuthorizedCredentials() {
         return preAuthorizedCredentials;
     }
 
@@ -136,7 +144,7 @@ public class CredentialOfferRequest extends AbstractOptionallyIdentifiedRequest
 
         final Map<String, Object> content;
         try {
-            content = new ObjectMapper().readValue(httpRequest.getQuery(), new TypeReference<Map<String, Object>>() {
+            content = new ObjectMapper().readValue(httpRequest.getBody(), new TypeReference<Map<String, Object>>() {
             });
         } catch (final IllegalArgumentException | JsonProcessingException e1) {
             throw new ParseException(e1.getMessage());
@@ -154,10 +162,9 @@ public class CredentialOfferRequest extends AbstractOptionallyIdentifiedRequest
 
         final Map<String, Object> preAuthorizedCredentialsInRequest = (Map<String, Object>) content
                 .get("preAuthorizedCredentials");
-        final Map<String, CredentialOfferRequestedCredential> preAuthorizedCredentials = new HashMap<>();
+        final Map<String, List<CredentialDataset>> preAuthorizedCredentials = new HashMap<>();
         for (final String key : preAuthorizedCredentialsInRequest.keySet()) {
-            preAuthorizedCredentials.put(key,
-                    CredentialOfferRequestedCredential.parse(preAuthorizedCredentialsInRequest.get(key)));
+            preAuthorizedCredentials.put(key, CredentialDataset.parse(preAuthorizedCredentialsInRequest.get(key)));
         }
 
         CredentialOfferTxCode credentialOfferTxCode = null;
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferSuccessResponse.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferSuccessResponse.java
index 73ce78a..2c98ccd 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferSuccessResponse.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferSuccessResponse.java
@@ -69,11 +69,12 @@ public class CredentialOfferSuccessResponse implements SuccessResponse {
      *
      * @param issuer                Credential Issuer identifier.
      * @param preAuthorizedCode     Pre-authorized code.
-     * @param preAuthCredentials    Credentials offered in the pre-authorized code flow.
+     * @param preAuthCredentials    Credentials offered in the pre-authorized code
+     *                              flow.
      * @param credentialOfferTxCode Transaction Code the wallet must present.
      */
     public CredentialOfferSuccessResponse(@Nonnull final String issuer, @Nonnull final String preAuthorizedCode,
-            @Nonnull final Map<String, CredentialOfferRequestedCredential> preAuthCredentials,
+            @Nonnull final Map<String, List<CredentialDataset>> preAuthCredentials,
             @Nullable final CredentialOfferTxCode credentialOfferTxCode) {
 
         credentialIssuer = issuer;
@@ -146,7 +147,7 @@ public class CredentialOfferSuccessResponse implements SuccessResponse {
      * Build the pre-authorized code grant of a Credential Offer.
      *
      * @param preauthCode pre-authorized code
-     * @param txCode Transaction Code the wallet must present, or null
+     * @param txCode      Transaction Code the wallet must present, or null
      * @return grant as a map
      */
     @SuppressWarnings("unchecked")
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCIAuthorizationDetail.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCIAuthorizationDetail.java
index c9c97fa..36f7bf9 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCIAuthorizationDetail.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCIAuthorizationDetail.java
@@ -117,6 +117,15 @@ public class OpenIDVCIAuthorizationDetail {
         return claims;
     }
 
+    /**
+     * Get credential identifiers.
+     *
+     * @return credential identifiers
+     */
+    public List<String> getCredentialIdentifiers() {
+        return credentialIdentifiers;
+    }
+
     /**
      * Serialize object to json.
      * 
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequest.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequest.java
index 229975c..fa550ee 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequest.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequest.java
@@ -159,17 +159,6 @@ public class OpenIDVCICredentialsRequest extends ProtectedResourceRequest {
         credentialResponseEncryption = responseEncryption;
     }
 
-    /**
-     * Validate request conforms to
-     * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request.
-     */
-    private void validate() {
-        if (credentialIdentifier != null && credentialConfigurationId != null) {
-            throw new IllegalArgumentException(
-                    "Request must not have both credential_identifier and credential_configuration_id fields");
-        }
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -192,7 +181,7 @@ public class OpenIDVCICredentialsRequest extends ProtectedResourceRequest {
         final AccessToken accessToken = AccessToken.parse(httpRequest);
         final Map<String, Object> payload;
         try {
-            payload = new ObjectMapper().readValue(httpRequest.getQuery(), new TypeReference<Map<String, Object>>() {
+            payload = new ObjectMapper().readValue(httpRequest.getBody(), new TypeReference<Map<String, Object>>() {
             });
         } catch (final JsonProcessingException e1) {
             throw new ParseException(e1.getMessage(),
@@ -211,7 +200,6 @@ public class OpenIDVCICredentialsRequest extends ProtectedResourceRequest {
                 (payload.get(CREDENTIAL_CONFIGURATION_ID_KEY) instanceof String value) ? value : null);
         request.setProofs((Map<String, Object>) payload.get(PROOFS_KEY));
         request.setCredentialResponseEncryption((Map<String, Object>) payload.get(CREDENTIAL_RESPONSE_ENCRYPTION_KEY));
-        request.validate();
         return request;
     }
 
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/AbstractOpenIDVCIConfiguration.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/AbstractOpenIDVCIConfiguration.java
index 0d6eafd..705181a 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/AbstractOpenIDVCIConfiguration.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/AbstractOpenIDVCIConfiguration.java
@@ -18,6 +18,7 @@ package org.geant.shibboleth.plugin.openidvci.profile.config.impl;
 
 import java.time.Duration;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -33,6 +34,7 @@ import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.annotation.constraint.Positive;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
 
 /** Base class for the OpenID4VCI profile configurations. */
 public abstract class AbstractOpenIDVCIConfiguration extends AbstractOIDCSSOConfiguration
@@ -76,6 +78,10 @@ public abstract class AbstractOpenIDVCIConfiguration extends AbstractOIDCSSOConf
     @Nonnull
     private Function<ProfileRequestContext, Function<ProfileRequestContext, String>> proofNonceGeneratorLookupStrategy;
 
+    /** Whether a credential configuration id is accepted when credential identifiers were issued. */
+    @Nonnull
+    private Predicate<ProfileRequestContext> allowCredentialConfigurationIdWithIdentifiersPredicate;
+
     /**
      * Constructor.
      *
@@ -92,6 +98,7 @@ public abstract class AbstractOpenIDVCIConfiguration extends AbstractOIDCSSOConf
         proofClaimsValidatorLookupStrategy = FunctionSupport.constant(null);
         keyAttestationClaimsValidatorLookupStrategy = FunctionSupport.constant(null);
         proofNonceGeneratorLookupStrategy = FunctionSupport.constant(null);
+        allowCredentialConfigurationIdWithIdentifiersPredicate = PredicateSupport.alwaysFalse();
     }
 
     /** {@inheritDoc} */
@@ -348,4 +355,33 @@ public abstract class AbstractOpenIDVCIConfiguration extends AbstractOIDCSSOConf
             @Nonnull final Function<ProfileRequestContext, Function<ProfileRequestContext, String>> strategy) {
         proofNonceGeneratorLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isAllowCredentialConfigurationIdWithIdentifiers(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        return allowCredentialConfigurationIdWithIdentifiersPredicate.test(profileRequestContext);
+    }
+
+    /**
+     * Set whether a credential configuration id is accepted when credential identifiers were issued.
+     *
+     * @param flag flag to set
+     */
+    public void setAllowCredentialConfigurationIdWithIdentifiers(final boolean flag) {
+        allowCredentialConfigurationIdWithIdentifiersPredicate =
+                flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+
+    /**
+     * Set condition for whether a credential configuration id is accepted when credential identifiers were
+     * issued.
+     *
+     * @param condition condition to set
+     */
+    public void setAllowCredentialConfigurationIdWithIdentifiersPredicate(
+            @Nonnull final Predicate<ProfileRequestContext> condition) {
+        allowCredentialConfigurationIdWithIdentifiersPredicate =
+                Constraint.isNotNull(condition, "Condition cannot be null");
+    }
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FilterPotentialCredentials.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FilterPotentialCredentials.java
new file mode 100644
index 0000000..262545e
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FilterPotentialCredentials.java
@@ -0,0 +1,146 @@
+/*
+ * 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.profile.impl;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
+import org.geant.shibboleth.plugin.openidvci.messaging.context.TokenContext;
+import org.geant.shibboleth.plugin.openidvci.messaging.context.navigate.AuthorizationDetailsLookupFunction;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCIAuthorizationDetail;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that limits {@link TokenContext#getPotentialCredentials()} to the credential configurations authorized
+ * for the grant, those whose 'scope' is in the granted scope of {@link OIDCAuthenticationResponseContext#getScope()}
+ * and those requested with 'authorization_details'.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class FilterPotentialCredentials extends AbstractCredentialValidationAction {
+
+    /** Class logger. */
+    @Nonnull
+    private Logger log = LoggerFactory.getLogger(FilterPotentialCredentials.class);
+
+    /** Strategy used to locate the {@link OpenIDVCIAuthorizationDetail} to use. */
+    @Nonnull
+    private Function<ProfileRequestContext, List<OpenIDVCIAuthorizationDetail>> authorizationDetailsStrategy;
+
+    /** Token context. */
+    @NonnullBeforeExec
+    private TokenContext tokenContext;
+
+    /** OIDC response context. */
+    @NonnullBeforeExec
+    private OIDCAuthenticationResponseContext oidcResponseContext;
+
+    /**
+     * Constructor.
+     */
+    public FilterPotentialCredentials() {
+        authorizationDetailsStrategy = new AuthorizationDetailsLookupFunction();
+    }
+
+    /**
+     * Set the strategy used to locate the {@link OpenIDVCIAuthorizationDetail} to use.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setAuthorizationDetailsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, List<OpenIDVCIAuthorizationDetail>> strategy) {
+        checkSetterPreconditions();
+        authorizationDetailsStrategy = Constraint.isNotNull(strategy,
+                "AuthorizationDetailsLookupStrategy lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        if (profileRequestContext.getInboundMessageContext() == null) {
+            log.error("{} No inbound message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        tokenContext = profileRequestContext.getInboundMessageContext().getSubcontext(TokenContext.class);
+        if (tokenContext == null || tokenContext.getPotentialCredentials() == null) {
+            log.debug("{} No potential credentials, nothing to do", getLogPrefix());
+            return false;
+        }
+        final MessageContext outboundMessageCtx = profileRequestContext.getOutboundMessageContext();
+        oidcResponseContext = outboundMessageCtx != null
+                ? outboundMessageCtx.getSubcontext(OIDCAuthenticationResponseContext.class) : null;
+        if (oidcResponseContext == null) {
+            log.error("{} No oidc response context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final Set<String> authorized = new HashSet<>();
+        final List<OpenIDVCIAuthorizationDetail> details = authorizationDetailsStrategy.apply(profileRequestContext);
+        if (details != null) {
+            details.forEach(detail -> authorized.add(detail.getCredentialConfigurationId()));
+        }
+        final Scope scope = oidcResponseContext.getScope();
+        if (scope != null) {
+            for (final Map.Entry<String, CredentialConfiguration> entry : getCredentialConfigurations().entrySet()) {
+                final String configurationScope = entry.getValue().getScope();
+                if (configurationScope != null && scope.contains(configurationScope)) {
+                    authorized.add(entry.getKey());
+                }
+            }
+        }
+        final ClaimsSet filtered = new ClaimsSet();
+        tokenContext.getPotentialCredentials().toJSONObject().forEach((name, value) -> {
+            if (authorized.contains(name)) {
+                filtered.setClaim(name, value);
+            } else {
+                log.debug("{} Credential configuration {} is not authorized, removed", getLogPrefix(), name);
+            }
+        });
+        tokenContext.setPotentialCredentials(filtered);
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetAuthorizationDetailsToResponseContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetAuthorizationDetailsToResponseContext.java
index 957b2ea..4a4a78c 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetAuthorizationDetailsToResponseContext.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetAuthorizationDetailsToResponseContext.java
@@ -17,15 +17,22 @@
 package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.geant.shibboleth.plugin.openidvci.messaging.context.TokenContext;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.navigate.AuthorizationDetailsLookupFunction;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCIAuthorizationDetail;
 import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
+import org.geant.shibboleth.plugin.openidvci.profile.logic.OpenIDVCITokenManipulationStrategy;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -39,15 +46,10 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * Action seeks for claims in {@link TokenContext#getPotentialCredentials()}
- * that match requested 'authorization_details' as described in
- * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-authorization-details.
- * Claim is considered a match if the claim name equals to requested
- * credential_configuration_id or is of format
- * [credential_configuration_id]_[postfix]. From matches we form
- * 'authorization_details' for response as described in
- * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response
- * now containing also 'credential_identifiers'.
+ * Action that forms the 'authorization_details' of the token response, as described in
+ * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response,
+ * from the credential datasets of {@link TokenContext#getPotentialCredentials()}. Credential identifiers are issued
+ * for the credential configurations requested with 'authorization_details' and for those having several datasets.
  */
 public class SetAuthorizationDetailsToResponseContext extends AbstractProfileAction {
 
@@ -60,7 +62,7 @@ public class SetAuthorizationDetailsToResponseContext extends AbstractProfileAct
     private Function<ProfileRequestContext, List<OpenIDVCIAuthorizationDetail>> authorizationDetailsStrategy;
 
     /** Authorization details of the request. */
-    @NonnullBeforeExec
+    @Nullable
     private List<OpenIDVCIAuthorizationDetail> authorizationDetails;
 
     /** Token context. */
@@ -92,85 +94,115 @@ public class SetAuthorizationDetailsToResponseContext extends AbstractProfileAct
         if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
+        if (profileRequestContext.getInboundMessageContext() == null) {
+            log.error("{} No inbound message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
         tokenContext = profileRequestContext.getInboundMessageContext().getSubcontext(TokenContext.class);
         if (tokenContext == null) {
             log.error("{} No token context", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
+        if (tokenContext.getPotentialCredentials() == null) {
+            log.debug("{} No potential credentials, nothing to do", getLogPrefix());
+            return false;
+        }
         authorizationDetails = authorizationDetailsStrategy.apply(profileRequestContext);
-        if (authorizationDetails == null) {
-            setDetailsForPreAuthzAsIfItWasRequested();
+        return true;
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final Map<String, Object> potentialCredentials;
+        try {
+            potentialCredentials = tokenContext.getPotentialCredentials().toJWTClaimsSet().toJSONObject();
+        } catch (final ParseException e) {
+            log.error("{} Parsing exception. Not able to match request to offering", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
+            return;
         }
-        if (authorizationDetails == null) {
-            log.debug("{} No authorization details, nothing to do", getLogPrefix());
-            return false;
+        final Set<String> requested = new LinkedHashSet<>();
+        if (authorizationDetails != null) {
+            authorizationDetails.forEach(detail -> requested.add(detail.getCredentialConfigurationId()));
         }
-        if (profileRequestContext.getInboundMessageContext() == null) {
-            log.error("{} No inbound message context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
+        final List<OpenIDVCIAuthorizationDetail> responseDetails = new ArrayList<>();
+        final Map<String, Object> issued = new LinkedHashMap<>();
+        for (final String configurationId : requested) {
+            addDetail(configurationId, datasetsOf(potentialCredentials, configurationId), responseDetails, issued);
+        }
+        if (!requested.isEmpty() && responseDetails.isEmpty()) {
+            log.error("{} No credentials available for request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
+            return;
+        }
+        for (final String configurationId : potentialCredentials.keySet()) {
+            if (requested.contains(configurationId)) {
+                continue;
+            }
+            final List<CredentialDataset> datasets = datasetsOf(potentialCredentials, configurationId);
+            if (datasets != null && datasets.size() > 1) {
+                addDetail(configurationId, datasets, responseDetails, issued);
+            }
+        }
+        if (!responseDetails.isEmpty()) {
+            tokenContext.setResponseAuthorizationDetails(responseDetails);
+            tokenContext.setIssuedCredentialIdentifiers(issued);
         }
-        return true;
     }
+// Checkstyle: CyclomaticComplexity ON
 
     /**
-     * Most likely temporary method. My understanding of the latest version of the
-     * specification is that authorization_details needs not to be in Token Response
-     * if it was never in any request. This method is now only for development phase
-     * inter-operability challenges. Please add a proper switch for this option if
-     * it remains.
+     * Add the authorization detail of a credential configuration and record its credential identifiers.
+     *
+     * @param configurationId credential configuration id
+     * @param datasets datasets of the credential configuration
+     * @param responseDetails authorization details of the response
+     * @param issued issued credential identifiers
      */
-    private void setDetailsForPreAuthzAsIfItWasRequested() {
-        if (tokenContext.getCredentialOfferObject() == null
-                || tokenContext.getCredentialOfferObject().getPreAuthorizedCredentials() == null) {
+    private void addDetail(@Nonnull final String configurationId, @Nullable final List<CredentialDataset> datasets,
+            @Nonnull final List<OpenIDVCIAuthorizationDetail> responseDetails,
+            @Nonnull final Map<String, Object> issued) {
+        if (datasets == null || datasets.isEmpty()) {
             return;
         }
-        final List<OpenIDVCIAuthorizationDetail> details = new ArrayList<>();
-        tokenContext.getCredentialOfferObject().getPreAuthorizedCredentials().keySet()
-                .forEach(key -> details.add(new OpenIDVCIAuthorizationDetail("openid_credential", key, null, null)));
-
-        if (details.size() != 0) {
-            authorizationDetails = details;
+        final List<String> identifiers = new ArrayList<>();
+        for (final CredentialDataset dataset : datasets) {
+            final String identifier = CredentialDataset.credentialIdentifier(configurationId, dataset);
+            identifiers.add(identifier);
+            final Map<String, Object> issuedRecord = new LinkedHashMap<>();
+            issuedRecord.put(OpenIDVCITokenManipulationStrategy.CREDENTIAL_CONFIGURATION_ID, configurationId);
+            if (dataset.getDatasetId() != null) {
+                issuedRecord.put(CredentialDataset.DATASET_ID, dataset.getDatasetId());
+            }
+            issued.put(identifier, issuedRecord);
         }
+        responseDetails.add(new OpenIDVCIAuthorizationDetail("openid_credential", configurationId, null, identifiers));
     }
 
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        // tokenContext.getCredentialOfferObject().
-        final List<OpenIDVCIAuthorizationDetail> matchedDetails = new ArrayList<>();
-        for (final OpenIDVCIAuthorizationDetail detail : authorizationDetails) {
-            final List<String> configurationsIds = new ArrayList<>();
-            final String configurationId = detail.getCredentialConfigurationId();
-            try {
-                for (final String credentialName : tokenContext.getPotentialCredentials().toJWTClaimsSet()
-                        .toJSONObject().keySet()) {
-                    if (!credentialName.startsWith(configurationId)) {
-                        continue;
-                    }
-                    final String potentialIdPart = credentialName.replaceFirst(configurationId, "");
-                    if (potentialIdPart.isEmpty()
-                            || potentialIdPart.startsWith("_") && potentialIdPart.lastIndexOf("_") == 0) {
-                        configurationsIds.add(credentialName);
-                    }
-                }
-                if (configurationsIds.size() > 0) {
-                    matchedDetails.add(new OpenIDVCIAuthorizationDetail(detail.getType(),
-                            detail.getCredentialConfigurationId(), null, configurationsIds));
-                }
-            } catch (final ParseException e) {
-                log.error("{} Parsing exception. Not able to match request to offering", getLogPrefix(), e);
-                ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
-                return;
-            }
+    /**
+     * Get the datasets of a credential configuration.
+     *
+     * @param potentialCredentials potential credentials
+     * @param configurationId credential configuration id
+     * @return datasets, or null if there are none
+     */
+    @Nullable
+    private List<CredentialDataset> datasetsOf(@Nonnull final Map<String, Object> potentialCredentials,
+            @Nonnull final String configurationId) {
+        final Object value = potentialCredentials.get(configurationId);
+        if (value == null) {
+            return null;
         }
-        if (matchedDetails.isEmpty()) {
-            log.error("{} No credentials available for request", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
-            return;
+        try {
+            return CredentialDataset.parse(value);
+        } catch (final ParseException e) {
+            log.debug("{} Claim {} is not a credential", getLogPrefix(), configurationId);
+            return null;
         }
-        tokenContext.setResponseAuthorizationDetails(matchedDetails);
     }
 
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetDPoPProofThumbprintToResponseContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetDPoPProofThumbprintToResponseContext.java
new file mode 100644
index 0000000..1b187aa
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetDPoPProofThumbprintToResponseContext.java
@@ -0,0 +1,76 @@
+/*
+ * 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.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.AbstractOpenIDVCITokenResponseAction;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultDPoPProofThumbprintLookupFunction;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that sets the thumbprint of the validated DPoP proof key to {@link OIDCAuthenticationResponseContext}, the
+ * key the access token of a pre-authorized code grant is bound to.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class SetDPoPProofThumbprintToResponseContext extends AbstractOpenIDVCITokenResponseAction {
+
+    /** Class logger. */
+    @Nonnull
+    private Logger log = LoggerFactory.getLogger(SetDPoPProofThumbprintToResponseContext.class);
+
+    /** Strategy used to read the thumbprint of the validated DPoP proof. */
+    @Nonnull
+    private Function<ProfileRequestContext, String> dpopProofThumbprintLookupStrategy =
+            new DefaultDPoPProofThumbprintLookupFunction();
+
+    /**
+     * Set the strategy used to read the thumbprint of the validated DPoP proof.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setDpopProofThumbprintLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        dpopProofThumbprintLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+        assert oidcResponseContext != null;
+        final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
+        if (proofThumbprint == null) {
+            log.debug("{} No DPoP proof, access token is not bound", getLogPrefix());
+            return;
+        }
+        oidcResponseContext.setDpopProofJwkThumbprint(proofThumbprint);
+        log.debug("{} Access token is bound to DPoP proof key {}", getLogPrefix(), proofThumbprint);
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/UnwrapGrant.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/UnwrapGrant.java
index 91e8c76..e1a65dd 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/UnwrapGrant.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/UnwrapGrant.java
@@ -25,6 +25,7 @@ import javax.annotation.Nonnull;
 
 import org.geant.shibboleth.plugin.openidvci.messaging.context.TokenContext;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.AbstractOpenIDVCITokenResponseAction;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.PreAuthorizedCodeGrant;
 import org.geant.shibboleth.plugin.openidvci.profile.context.navigate.APIRequestClientIDLookupFunction;
 import org.geant.shibboleth.plugin.openidvci.storage.CredentialOfferCache;
@@ -284,7 +285,7 @@ public class UnwrapGrant extends AbstractOpenIDVCITokenResponseAction {
         final ClaimsSet credentialOfferClaims = new ClaimsSet();
         ctx.getCredentialOfferObject().getPreAuthorizedCredentials().forEach((key, value) -> {
             try {
-                credentialOfferClaims.setClaim(key, value.serialize());
+                credentialOfferClaims.setClaim(key, CredentialDataset.serialize(value));
             } catch (final JsonProcessingException e) {
                 log.warn("{} Error occurred while handling CredentialOfferObject {}", getLogPrefix(), e);
 
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 6d401d7..e9adc6b 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
@@ -18,7 +18,6 @@ package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import javax.annotation.Nonnull;
 
@@ -26,6 +25,7 @@ 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.CredentialDataset;
 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;
@@ -51,10 +51,6 @@ public class ValidateCredentialOffering extends AbstractCredentialValidationActi
     @Nonnull
     private Logger log = LoggerFactory.getLogger(ValidateCredentialOffering.class);
 
-    /** The requested credential configuration identifiers . */
-    @NonnullBeforeExec
-    private Set<String> credentialConfIds;
-
     /**
      * Inbound {@link CredentialOfferRequest}.
      */
@@ -76,33 +72,29 @@ public class ValidateCredentialOffering extends AbstractCredentialValidationActi
             ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
             return false;
         }
-        credentialConfIds = request.getPreAuthorizedCredentials().keySet();
         return super.doPreExecute(profileRequestContext);
     }
 
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        credentialConfIds.forEach(id -> {
-            if (!getCredentialConfigurations().keySet().contains(id)) {
-                log.error("{} Requested credential configuration id {} is not supported", getLogPrefix(), id);
-                ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
-                return;
-            }
-        });
-
-        for (final Map.Entry<String, CredentialOfferRequestedCredential> entry
-                : request.getPreAuthorizedCredentials().entrySet()) {
+        for (final Map.Entry<String, List<CredentialDataset>> 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(),
+                log.error("{} Requested credential configuration id {} is not supported", getLogPrefix(),
                         entry.getKey());
                 ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
                 return;
             }
+            for (final CredentialDataset dataset : entry.getValue()) {
+                if (!hasAllMandatoryClaims(configuration, dataset.getContent())) {
+                    log.error("{} Requested credential {} is missing one or more mandatory claims", getLogPrefix(),
+                            entry.getKey());
+                    ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
+                    return;
+                }
+            }
         }
 
         final CredentialOfferContext ctx = new CredentialOfferContext();
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateRequestedCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateRequestedCredential.java
index 934c648..42a6b44 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateRequestedCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateRequestedCredential.java
@@ -17,35 +17,39 @@
 package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
 import java.text.ParseException;
+import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.navigate.RequestCredentialConfigurationIdLookupFunction;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.navigate.RequestCredentialIdentifierLookupFunction;
-import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
 import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
+import org.geant.shibboleth.plugin.openidvci.profile.config.OpenIDVCIConfiguration;
 import org.geant.shibboleth.plugin.openidvci.profile.logic.OpenIDVCITokenManipulationStrategy;
+import org.geant.shibboleth.plugin.openidvci.profile.logic.PotentialCredentialLookupFunction;
 import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
 import net.shibboleth.shared.logic.Constraint;
 
 /**
- * Validates requested credential has a credential configuration. Both requested
- * credential and the configuration is stored to {@link CredentialsContext}
+ * Validates the requested credential against the credential identifiers issued in the token response and the
+ * credential configurations. The requested credential and its configuration are stored to
+ * {@link CredentialsContext}.
  */
 public class ValidateRequestedCredential extends AbstractCredentialValidationAction {
 
@@ -67,15 +71,28 @@ public class ValidateRequestedCredential extends AbstractCredentialValidationAct
     @Nonnull
     private Function<ProfileRequestContext, String> requestCredentialIdentifierLookupStrategy;
 
+    /** Strategy used to obtain the datasets of a credential configuration. */
+    @Nonnull
+    private BiFunction<ProfileRequestContext, String, List<CredentialDataset>> credentialContentLookupStrategy;
+
+    /** Strategy used to locate the {@link RelyingPartyContext}. */
+    @Nonnull
+    private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextLookupStrategy;
+
     /** OIDC response context. */
     private OIDCAuthenticationResponseContext oidcResponseContext;
 
+    /** Whether a credential configuration id is accepted when credential identifiers were issued. */
+    private boolean allowCredentialConfigurationIdWithIdentifiers;
+
     /**
      * Constructor.
      */
     public ValidateRequestedCredential() {
         requestCredentialConfigurationIdLookupStrategy = new RequestCredentialConfigurationIdLookupFunction();
         requestCredentialIdentifierLookupStrategy = new RequestCredentialIdentifierLookupFunction();
+        credentialContentLookupStrategy = new PotentialCredentialLookupFunction();
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
     }
 
     /**
@@ -92,7 +109,7 @@ public class ValidateRequestedCredential extends AbstractCredentialValidationAct
     }
 
     /**
-     * Set the strategy used to obtain the credential configuration id value from
+     * Set the strategy used to obtain the credential identifier value from
      * credentials request.
      * 
      * @param strategy lookup strategy
@@ -104,6 +121,31 @@ public class ValidateRequestedCredential extends AbstractCredentialValidationAct
                 "RequestCredentialIdentifierLookupStrategy lookup strategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to obtain the datasets of a credential configuration.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setCredentialContentLookupStrategy(
+            @Nonnull final BiFunction<ProfileRequestContext, String, List<CredentialDataset>> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        credentialContentLookupStrategy = Constraint.isNotNull(strategy,
+                "CredentialContentLookupStrategy lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the {@link RelyingPartyContext} associated with a given
+     * {@link ProfileRequestContext}.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        relyingPartyContextLookupStrategy = Constraint.isNotNull(strategy,
+                "RelyingPartyContext lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -117,73 +159,118 @@ public class ValidateRequestedCredential extends AbstractCredentialValidationAct
             return false;
         }
         oidcResponseContext = outboundMessageCtx.getSubcontext(OIDCAuthenticationResponseContext.class);
-        if (oidcResponseContext == null) {
+        if (oidcResponseContext == null || oidcResponseContext.getAuthorizationGrantClaimsSet() == null) {
             log.error("{} No oidc response context", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         }
+        final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+        allowCredentialConfigurationIdWithIdentifiers = rpCtx != null
+                && rpCtx.getProfileConfig() instanceof OpenIDVCIConfiguration configuration
+                && configuration.isAllowCredentialConfigurationIdWithIdentifiers(profileRequestContext);
         return true;
     }
 
-// Checkstyle: CyclomaticComplexity OFF
+// Checkstyle: CyclomaticComplexity|ReturnCount OFF
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
-        String idInRequest = requestCredentialConfigurationIdLookupStrategy.apply(profileRequestContext);
-        if (idInRequest == null) {
-            // TOOD: Not sure if we should treat both values with same logic.
-            idInRequest = requestCredentialIdentifierLookupStrategy.apply(profileRequestContext);
+        final String configurationId = requestCredentialConfigurationIdLookupStrategy.apply(profileRequestContext);
+        final String identifier = requestCredentialIdentifierLookupStrategy.apply(profileRequestContext);
+        if ((configurationId == null) == (identifier == null)) {
+            log.error("{} Request must have either credential_identifier or credential_configuration_id",
+                    getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIAL_REQUEST);
+            return;
         }
-        CredentialConfiguration configuration = null;
-        if (idInRequest != null && getCredentialConfigurations().containsKey(idInRequest)) {
-            configuration = getCredentialConfigurations().get(idInRequest);
+        final Map<String, Object> issuedIdentifiers;
+        try {
+            issuedIdentifiers = oidcResponseContext.getAuthorizationGrantClaimsSet().getClaimsSet()
+                    .getJSONObjectClaim(OpenIDVCITokenManipulationStrategy.CREDENTIAL_IDENTIFIERS);
+        } catch (final ParseException e) {
+            log.error("{} Parsing issued credential identifiers failed", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
         }
-        if (configuration == null) {
-            if (idInRequest != null) {
-                final String newId = idInRequest;
-                // Look for configurations that start as "id_"
-                final Set<String> set = getCredentialConfigurations().keySet().stream()
-                        .map(s -> newId.replaceFirst(s, ""))
-                        .filter(s -> s.startsWith("_") && s.lastIndexOf("_") == 0).collect(Collectors.toSet());
-                if (set.size() == 1) {
-                    configuration = getCredentialConfigurations().get(set.iterator().next());
-                }
-            }
+        final String issuedConfigurationId;
+        final String issuedDatasetId;
+        if (identifier != null) {
+            final Map<?, ?> issued = issuedIdentifiers != null
+                    && issuedIdentifiers.get(identifier) instanceof Map<?, ?> issuedRecord ? issuedRecord : null;
+            issuedConfigurationId = issued != null
+                    && issued.get(OpenIDVCITokenManipulationStrategy.CREDENTIAL_CONFIGURATION_ID) instanceof String id
+                            ? id : null;
+            issuedDatasetId = issued != null && issued.get(CredentialDataset.DATASET_ID) instanceof String id ? id
+                    : null;
+        } else {
+            issuedConfigurationId = null;
+            issuedDatasetId = null;
+        }
+        final String selectedConfigurationId = identifier != null ? issuedConfigurationId : configurationId;
+        if (identifier != null && issuedConfigurationId == null) {
+            log.error("{} Credential identifier {} was not issued", getLogPrefix(), identifier);
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.UNKNOWN_CREDENTIAL_IDENTIFIER);
+            return;
+        }
+        if (identifier == null && issuedIdentifiers != null && !issuedIdentifiers.isEmpty()
+                && !allowCredentialConfigurationIdWithIdentifiers) {
+            log.error("{} Request has credential_configuration_id {} although credential identifiers were issued",
+                    getLogPrefix(), configurationId);
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIAL_REQUEST);
+            return;
         }
+        final CredentialConfiguration configuration = getCredentialConfigurations().get(selectedConfigurationId);
         if (configuration == null) {
-            log.error("{} No matching configuration found for requested credential {}", getLogPrefix(), idInRequest);
-            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
+            log.error("{} Credential configuration {} is unknown", getLogPrefix(), selectedConfigurationId);
+            ActionSupport.buildEvent(profileRequestContext, identifier != null
+                    ? OpenIDVCIEventIds.UNKNOWN_CREDENTIAL_IDENTIFIER : OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
             return;
         }
-        // look for claim that would match id and store it.
-        CredentialOfferRequestedCredential credential = null;
-        try {
-            final Map<String, Object> potentialClaims =
-                    oidcResponseContext.getAuthorizationGrantClaimsSet().getClaimsSet()
-                    .getJSONObjectClaim(OpenIDVCITokenManipulationStrategy.POTENTIAL_CLAIMS);
-            if (potentialClaims != null && potentialClaims.containsKey(idInRequest)) {
-                credential = CredentialOfferRequestedCredential.parse((String)potentialClaims.get(idInRequest));
-            }
-        } catch (final ParseException | com.nimbusds.oauth2.sdk.ParseException | JsonProcessingException e) {
-            log.error("{} parsing failed", getLogPrefix(), e);
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+        final List<CredentialDataset> datasets =
+                credentialContentLookupStrategy.apply(profileRequestContext, selectedConfigurationId);
+        if (identifier == null && datasets != null && datasets.size() > 1) {
+            log.error("{} Credential configuration {} has several datasets, credential_identifier is required",
+                    getLogPrefix(), configurationId);
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIAL_REQUEST);
             return;
         }
-        if (credential == null) {
+        final CredentialDataset dataset = selectDataset(datasets, issuedDatasetId);
+        if (dataset == null) {
             log.error("{} No matching credential content found for requested credential {}", getLogPrefix(),
-                    idInRequest);
-            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
+                    identifier != null ? identifier : configurationId);
+            ActionSupport.buildEvent(profileRequestContext, identifier != null
+                    ? OpenIDVCIEventIds.UNKNOWN_CREDENTIAL_IDENTIFIER : OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
             return;
         }
-        // TODO: Move context creation to initialization phase
         final CredentialsContext ctx = profileRequestContext.getInboundMessageContext()
                 .ensureSubcontext(CredentialsContext.class);
-        ctx.setCredentialContent(credential);
+        ctx.setCredentialContent(dataset.getContent());
         ctx.setCredentialConfiguration(configuration);
-        // TODO: This should be value in configuration. Fix.
-        ctx.setCredentialIdentifier(idInRequest);
+        ctx.setCredentialIdentifier(identifier != null ? identifier : configurationId);
+        ctx.setCredentialConfigurationId(selectedConfigurationId);
+        ctx.setDatasetId(dataset.getDatasetId());
+    }
+// Checkstyle: CyclomaticComplexity|ReturnCount ON
+
+    /**
+     * Select the dataset of the request.
+     *
+     * @param datasets datasets of the credential configuration
+     * @param datasetId dataset id the credential identifier was issued for, or null
+     * @return dataset, or null if there is no such dataset
+     */
+    @Nullable
+    private CredentialDataset selectDataset(@Nullable final List<CredentialDataset> datasets,
+            @Nullable final String datasetId) {
+        if (datasets == null || datasets.isEmpty()) {
+            return null;
+        }
+        if (datasetId == null) {
+            return datasets.size() == 1 ? datasets.get(0) : null;
+        }
+        return datasets.stream().filter(dataset -> datasetId.equals(dataset.getDatasetId())).findFirst()
+                .orElse(null);
     }
 
-// Checkstyle: CyclomaticComplexity ON
-}
\ No newline at end of file
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialDetailsLookupFunction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialDetailsLookupFunction.java
index d069dac..8c735b9 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialDetailsLookupFunction.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialDetailsLookupFunction.java
@@ -55,7 +55,10 @@ public class CredentialDetailsLookupFunction implements Function<ProfileRequestC
             return details;
         }
 
-        details.put("credential_configuration_id", ctx.getCredentialIdentifier());
+        details.put("credential_configuration_id", ctx.getCredentialConfigurationId());
+        if (ctx.getDatasetId() != null) {
+            details.put("dataset_id", ctx.getDatasetId());
+        }
         if (ctx.getCredentialConfiguration() != null) {
             details.put("format", ctx.getCredentialConfiguration().getFormat());
         }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/OpenIDVCITokenManipulationStrategy.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/OpenIDVCITokenManipulationStrategy.java
index b0dab50..5926a17 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/OpenIDVCITokenManipulationStrategy.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/OpenIDVCITokenManipulationStrategy.java
@@ -42,7 +42,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * 
  * 1) authorization_details and stores it to token, usually either authorization
  * code or access token, 2) potential verifiable credentials, stores them to
- * access token.
+ * access token, 3) issued credential identifiers, stores them to access token.
  *
  */
 public class OpenIDVCITokenManipulationStrategy
@@ -54,6 +54,12 @@ public class OpenIDVCITokenManipulationStrategy
     /** Claim name carrying the potential claims. */
     public static final String POTENTIAL_CLAIMS = "vci.int.potentialClaims";
 
+    /** Claim name carrying the issued credential identifiers. */
+    public static final String CREDENTIAL_IDENTIFIERS = "vci.int.credentialIdentifiers";
+
+    /** Member name of the credential configuration id of an issued credential identifier. */
+    public static final String CREDENTIAL_CONFIGURATION_ID = "credential_configuration_id";
+
     /** Class logger. */
     @Nonnull
     private Logger log = LoggerFactory.getLogger(OpenIDVCITokenManipulationStrategy.class);
@@ -101,6 +107,10 @@ public class OpenIDVCITokenManipulationStrategy
                 profileRequestContext.getInboundMessageContext().getSubcontext(TokenContext.class);
         if (tokenContext != null) {
             claims.put(POTENTIAL_CLAIMS, tokenContext.getPotentialCredentials().toJSONObject());
+            if (tokenContext.getIssuedCredentialIdentifiers() != null
+                    && !tokenContext.getIssuedCredentialIdentifiers().isEmpty()) {
+                claims.put(CREDENTIAL_IDENTIFIERS, tokenContext.getIssuedCredentialIdentifiers());
+            }
         }
         return claims;
     }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PotentialCredentialLookupFunction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PotentialCredentialLookupFunction.java
new file mode 100644
index 0000000..bf83292
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PotentialCredentialLookupFunction.java
@@ -0,0 +1,99 @@
+/*
+ * 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.profile.logic;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Function that returns the datasets of a credential configuration from the
+ * potential claims of the access token.
+ */
+public class PotentialCredentialLookupFunction
+        implements BiFunction<ProfileRequestContext, String, List<CredentialDataset>> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(PotentialCredentialLookupFunction.class);
+
+    /** Strategy used to locate the {@link OIDCAuthenticationResponseContext}. */
+    @Nonnull
+    private Function<ProfileRequestContext, OIDCAuthenticationResponseContext> oidcResponseContextLookupStrategy;
+
+    /** Constructor. */
+    public PotentialCredentialLookupFunction() {
+        final Function<ProfileRequestContext, OIDCAuthenticationResponseContext> strategy = new ChildContextLookup<>(
+                OIDCAuthenticationResponseContext.class).compose(new OutboundMessageContextLookup());
+        assert strategy != null;
+        oidcResponseContextLookupStrategy = strategy;
+    }
+
+    /**
+     * Set the strategy used to locate the
+     * {@link OIDCAuthenticationResponseContext}.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setOIDCResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationResponseContext> strategy) {
+        oidcResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "OIDCAuthenticationResponseContext lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public List<CredentialDataset> apply(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nullable final String configurationId) {
+        if (profileRequestContext == null || configurationId == null) {
+            return null;
+        }
+        final OIDCAuthenticationResponseContext oidcResponseContext = oidcResponseContextLookupStrategy
+                .apply(profileRequestContext);
+        if (oidcResponseContext == null || oidcResponseContext.getAuthorizationGrantClaimsSet() == null) {
+            log.debug("No authorization grant to read potential credentials from");
+            return null;
+        }
+        try {
+            final Map<String, Object> potentialClaims = oidcResponseContext.getAuthorizationGrantClaimsSet()
+                    .getClaimsSet().getJSONObjectClaim(OpenIDVCITokenManipulationStrategy.POTENTIAL_CLAIMS);
+            if (potentialClaims != null && potentialClaims.get(configurationId) != null) {
+                return CredentialDataset.parse(potentialClaims.get(configurationId));
+            }
+        } catch (final ParseException | com.nimbusds.oauth2.sdk.ParseException e) {
+            log.error("Parsing potential credential {} failed", configurationId, e);
+        }
+        return null;
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListAssignmentsRequest.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListAssignmentsRequest.java
index fb42df4..631fd0c 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListAssignmentsRequest.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListAssignmentsRequest.java
@@ -144,7 +144,7 @@ public class StatusListAssignmentsRequest extends AbstractOptionallyIdentifiedRe
 
         final Map<String, Object> payload;
         try {
-            payload = new ObjectMapper().readValue(httpRequest.getQuery(),
+            payload = new ObjectMapper().readValue(httpRequest.getBody(),
                     new TypeReference<Map<String, Object>>() {
                     });
         } catch (final JsonProcessingException e) {
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListRevokeRequest.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListRevokeRequest.java
index ac1e579..eb86372 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListRevokeRequest.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/messaging/impl/StatusListRevokeRequest.java
@@ -136,7 +136,7 @@ public class StatusListRevokeRequest extends AbstractOptionallyIdentifiedRequest
 
         final Map<String, Object> payload;
         try {
-            payload = new ObjectMapper().readValue(httpRequest.getQuery(), new TypeReference<Map<String, Object>>() {
+            payload = new ObjectMapper().readValue(httpRequest.getBody(), new TypeReference<Map<String, Object>>() {
             });
         } catch (final JsonProcessingException e) {
             throw new ParseException(e.getMessage(),
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObject.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObject.java
index d224f1d..147af54 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObject.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObject.java
@@ -22,7 +22,7 @@ import java.util.Map;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferTxCode;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
@@ -42,7 +42,7 @@ public class CredentialOfferObject {
 
     /** Credential offer request content in pre-auth flow. */
     @Nullable
-    private final Map<String, CredentialOfferRequestedCredential> preAuthorizedCredentials;
+    private final Map<String, List<CredentialDataset>> preAuthorizedCredentials;
 
     /** List of credentials for Credential offer in code flow. */
     @Nullable
@@ -64,7 +64,7 @@ public class CredentialOfferObject {
     public CredentialOfferObject(
             @JsonProperty("clientID") @Nullable final ClientID clientID,
             @JsonProperty("preAuthorizedCredentials") @Nullable
-            final Map<String, CredentialOfferRequestedCredential> preAuthorizedCredentials,
+            final Map<String, List<CredentialDataset>> preAuthorizedCredentials,
             @JsonProperty("codeCredentials") @Nullable final List<String> codeCredentials,
             @JsonProperty("credentialOfferTxCode") @Nullable final CredentialOfferTxCode credentialOfferTxCode) {
         this.clientID = clientID;
@@ -89,7 +89,7 @@ public class CredentialOfferObject {
      * 
      * @return credential offer request content in pre-auth flow.
      */
-    public Map<String, CredentialOfferRequestedCredential> getPreAuthorizedCredentials() {
+    public Map<String, List<CredentialDataset>> getPreAuthorizedCredentials() {
         return preAuthorizedCredentials;
     }
 
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
index 226ae40..af9b300 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
@@ -185,6 +185,8 @@
             value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).UNKNOWN_CREDENTIAL_CONFIGURATION}" />
         <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).NO_CREDENTIALS_FOR_REQUEST}"
             value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).UNKNOWN_CREDENTIAL_CONFIGURATION}" />
+        <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).UNKNOWN_CREDENTIAL_IDENTIFIER}"
+            value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).UNKNOWN_CREDENTIAL_IDENTIFIER}" />
         <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).PROOF_TYPE_UNSUPPORTED}"
             value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).INVALID_PROOF}" />
         <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).INVALID_PROOF}"
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-beans.xml
index fe12a93..bf57af6 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-beans.xml
@@ -844,9 +844,17 @@
     <bean id="OpenIDVCISetAuthorizationDetailsToResponseContext" class="org.geant.shibboleth.plugin.openidvci.profile.impl.SetAuthorizationDetailsToResponseContext"
         scope="prototype"/>
 
+    <bean id="OpenIDVCIFilterPotentialCredentials"
+        class="org.geant.shibboleth.plugin.openidvci.profile.impl.FilterPotentialCredentials" scope="prototype"
+        p:credentialConfigurationsResolver-ref="openidvci.CredentialConfigurationsResolver" />
+
     <bean id="OpenIDVCIConsumeCredentialOffer" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ConsumeCredentialOffer"
         scope="prototype" p:credentialOfferCache-ref="openidvci.CredentialOfferCache" />
 
+    <bean id="OpenIDVCISetDPoPProofThumbprintToResponseContext"
+        class="org.geant.shibboleth.plugin.openidvci.profile.impl.SetDPoPProofThumbprintToResponseContext"
+        scope="prototype" />
+
     <!-- REPLACED InitializeAuthenticationContext of oidc/abstract-api. -->
     <bean id="InitializeAuthenticationContext" class="org.geant.shibboleth.plugin.openidvci.profile.impl.InitializeAuthenticationContext"
         scope="prototype" />
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-flow.xml
index 765eb7b..3db0498 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-flow.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-flow.xml
@@ -8,6 +8,8 @@
       OpenIDVCIValidatePreAuthorizedCode           one time use of a pre-authorized code
       OpenIDVCISetAuthorizationDetailsToResponseContext   credential authorization details
       OpenIDVCIConsumeCredentialOffer              retires a stored offer before the transaction code is read
+      OpenIDVCISetDPoPProofThumbprintToResponseContext   DPoP key the pre-authorized access token is bound to
+      OpenIDVCIFilterPotentialCredentials          credentials of the code grant limited to the granted scope and authorization_details
       OpenIDVCIBranchOnPreAuthorizedCodeGrant state   takes that grant aside
       OpenIDVCIPreAuthzGrantProcessing state       the pre-authorized code grant branch
       OpenIDVCIBuildTokensForCredentialsAccess state  the opaque access token of both grants
@@ -99,6 +101,8 @@
         <evaluate expression="ValidateScope" />
         <evaluate expression="ValidateAudience" />
         <evaluate expression="SetSessionIdFromAuthzCodeToResponseContext" />
+        <!-- ADDED. -->
+        <evaluate expression="OpenIDVCIFilterPotentialCredentials" />
         <evaluate expression="'proceed'" />
         
         <!-- REPLACED the transition to CheckTraditionalGrantForAudience. -->
@@ -117,6 +121,7 @@
         <evaluate expression="OpenIDVCIValidatePreAuthorizedCode" />
         <evaluate expression="OpenIDVCIConsumeCredentialOffer" />
         <evaluate expression="OpenIDVCIValidateTxCode" />
+        <evaluate expression="OpenIDVCISetDPoPProofThumbprintToResponseContext" />
         <evaluate expression="'proceed'" />
 
         <transition on="proceed" to="OpenIDVCIBuildTokensForCredentialsAccess" />
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/context/navigate/AuthorizationDetailsLookupFunctionTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/context/navigate/AuthorizationDetailsLookupFunctionTest.java
new file mode 100644
index 0000000..cb0b7b4
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/context/navigate/AuthorizationDetailsLookupFunctionTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.messaging.context.navigate;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCIAuthorizationDetail;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+
+/**
+ * Unit tests for {@link AuthorizationDetailsLookupFunction}.
+ */
+public class AuthorizationDetailsLookupFunctionTest {
+
+    private static final String DETAILS = "[{\"type\":\"openid_credential\",\"credential_configuration_id\":\"GeantIncubatorDiploma\"}]";
+
+    private ProfileRequestContext profileRequestCtx;
+
+    private AuthorizationDetailsLookupFunction lookup;
+
+    @BeforeMethod
+    protected void setUp() {
+        profileRequestCtx = new ProfileRequestContext();
+        profileRequestCtx.setInboundMessageContext(new MessageContext());
+        profileRequestCtx.setOutboundMessageContext(new MessageContext());
+        lookup = new AuthorizationDetailsLookupFunction();
+    }
+
+    private void setQueryRequest(final String details) {
+        final AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder(
+                new ResponseType(ResponseType.Value.CODE), new ClientID("client"))
+                .endpointURI(URI.create("https://issuer.example.org/idp/profile/oidc/authorize"));
+        if (details != null) {
+            builder.customParameter("authorization_details", details);
+        }
+        profileRequestCtx.getInboundMessageContext().setMessage(builder.build());
+    }
+
+    private void setPushedRequest(final Object details) {
+        profileRequestCtx.getInboundMessageContext()
+                .setMessage(new AuthorizationRequest.Builder(URI.create("urn:ietf:params:oauth:request_uri:abc"),
+                        new ClientID("client"))
+                        .endpointURI(URI.create("https://issuer.example.org/idp/profile/oidc/authorize")).build());
+        final JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder().claim("client_id", "client");
+        if (details != null) {
+            claims.claim("authorization_details", details);
+        }
+        profileRequestCtx.getOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class)
+                .setRequestObject(new PlainJWT(claims.build()));
+    }
+
+    @Test
+    public void testQueryParameter() {
+        setQueryRequest(DETAILS);
+        final List<OpenIDVCIAuthorizationDetail> details = lookup.apply(profileRequestCtx);
+        Assert.assertNotNull(details);
+        Assert.assertEquals(details.get(0).getCredentialConfigurationId(), "GeantIncubatorDiploma");
+    }
+
+    @Test
+    public void testNone() {
+        setQueryRequest(null);
+        Assert.assertNull(lookup.apply(profileRequestCtx));
+    }
+
+    @Test
+    public void testPushedAsString() {
+        setPushedRequest(DETAILS);
+        final List<OpenIDVCIAuthorizationDetail> details = lookup.apply(profileRequestCtx);
+        Assert.assertNotNull(details);
+        Assert.assertEquals(details.get(0).getCredentialConfigurationId(), "GeantIncubatorDiploma");
+    }
+
+    @Test
+    public void testPushedAsList() {
+        setPushedRequest(
+                List.of(Map.of("type", "openid_credential", "credential_configuration_id", "GeantIncubatorDiploma")));
+        final List<OpenIDVCIAuthorizationDetail> details = lookup.apply(profileRequestCtx);
+        Assert.assertNotNull(details);
+        Assert.assertEquals(details.get(0).getCredentialConfigurationId(), "GeantIncubatorDiploma");
+    }
+
+    @Test
+    public void testPushedWithoutDetails() {
+        setPushedRequest(null);
+        Assert.assertNull(lookup.apply(profileRequestCtx));
+    }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialDatasetTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialDatasetTest.java
new file mode 100644
index 0000000..c32cda2
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialDatasetTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.messaging.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.ParseException;
+
+/**
+ * Unit tests for {@link CredentialDataset}.
+ */
+public class CredentialDatasetTest {
+
+    private static final String CLAIMS = "[{\"path\":[\"degree\"],\"value\":\"BSc\"}]";
+
+    private static final String BSC = "{\"dataset_id\":\"bsc\",\"claims\":" + CLAIMS + "}";
+
+    private static final String MSC = "{\"dataset_id\":\"msc\",\"claims\":[{\"path\":[\"degree\"],\"value\":\"MSc\"}]}";
+
+    @Test
+    public void testClaims() throws Exception {
+        final List<CredentialDataset> datasets = CredentialDataset.parse(CLAIMS);
+        Assert.assertEquals(datasets.size(), 1);
+        Assert.assertNull(datasets.get(0).getDatasetId());
+        Assert.assertEquals(datasets.get(0).getContent().getRequestedCredential().get(0).getValue(), "BSc");
+    }
+
+    @Test
+    public void testRequestedCredentialWrapper() throws Exception {
+        final List<CredentialDataset> datasets = CredentialDataset.parse("{\"requestedCredential\":" + CLAIMS + "}");
+        Assert.assertEquals(datasets.size(), 1);
+        Assert.assertNull(datasets.get(0).getDatasetId());
+    }
+
+    @Test
+    public void testParsedClaims() throws Exception {
+        final List<CredentialDataset> datasets = CredentialDataset.parse(new ObjectMapper().readValue(CLAIMS,
+                Object.class));
+        Assert.assertEquals(datasets.size(), 1);
+    }
+
+    @Test
+    public void testDatasets() throws Exception {
+        final List<CredentialDataset> datasets = CredentialDataset.parse("[" + BSC + "," + MSC + "]");
+        Assert.assertEquals(datasets.size(), 2);
+        Assert.assertEquals(datasets.get(0).getDatasetId(), "bsc");
+        Assert.assertEquals(datasets.get(1).getContent().getRequestedCredential().get(0).getValue(), "MSc");
+    }
+
+    @Test
+    public void testDatasetsAsArrayOfStrings() throws Exception {
+        final List<CredentialDataset> datasets = CredentialDataset.parse(List.of(BSC, MSC));
+        Assert.assertEquals(datasets.size(), 2);
+        Assert.assertEquals(datasets.get(1).getDatasetId(), "msc");
+    }
+
+    @Test
+    public void testDatasetAsObject() throws Exception {
+        final List<CredentialDataset> datasets = CredentialDataset.parse(new ObjectMapper().readValue(BSC,
+                Map.class));
+        Assert.assertEquals(datasets.size(), 1);
+        Assert.assertEquals(datasets.get(0).getDatasetId(), "bsc");
+    }
+
+    @Test
+    public void testSerializeRoundTrip() throws Exception {
+        final List<CredentialDataset> datasets = CredentialDataset.parse("[" + BSC + "," + MSC + "]");
+        final List<CredentialDataset> parsed = CredentialDataset.parse(CredentialDataset.serialize(datasets));
+        Assert.assertEquals(parsed.size(), 2);
+        Assert.assertEquals(parsed.get(0).getDatasetId(), "bsc");
+        Assert.assertEquals(parsed.get(0).getContent().getRequestedCredential().get(0).getValue(), "BSc");
+    }
+
+    @Test
+    public void testSerializeSingle() throws Exception {
+        final List<CredentialDataset> parsed = CredentialDataset.parse(
+                CredentialDataset.serialize(CredentialDataset.parse(CLAIMS)));
+        Assert.assertEquals(parsed.size(), 1);
+        Assert.assertNull(parsed.get(0).getDatasetId());
+    }
+
+    @Test
+    public void testCredentialIdentifier() throws Exception {
+        Assert.assertEquals(CredentialDataset.credentialIdentifier("UniversityDegree",
+                CredentialDataset.parse(BSC).get(0)), "UniversityDegree#bsc");
+        Assert.assertEquals(CredentialDataset.credentialIdentifier("UniversityDegree",
+                CredentialDataset.parse(CLAIMS).get(0)), "UniversityDegree");
+    }
+
+    @Test(expectedExceptions = ParseException.class)
+    public void testSeveralWithoutId() throws Exception {
+        CredentialDataset.parse("[" + BSC + ",{\"claims\":" + CLAIMS + "}]");
+    }
+
+    @Test(expectedExceptions = ParseException.class)
+    public void testDuplicateId() throws Exception {
+        CredentialDataset.parse("[" + BSC + "," + BSC + "]");
+    }
+
+    @Test(expectedExceptions = ParseException.class)
+    public void testEmpty() throws Exception {
+        CredentialDataset.parse("[]");
+    }
+
+    @Test(expectedExceptions = ParseException.class)
+    public void testNotJson() throws Exception {
+        CredentialDataset.parse("member");
+    }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequestTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequestTest.java
index d8297db..1d6cfc5 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequestTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialOfferRequestTest.java
@@ -39,7 +39,7 @@ public class CredentialOfferRequestTest {
         httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"txCode\":{\"value\":\"12345\"}, \"preAuthorizedCredentials\":{\"GeantIncubatorSdJwt2\":[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"edu\",\"eppn\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"givenName\"],\"value\":\"Gemma Gemina Erasmus\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]}}");
 
     }
@@ -52,8 +52,8 @@ public class CredentialOfferRequestTest {
         Assert.assertTrue(request.getPreAuthorizedCredentials().containsKey("GeantIncubatorSdJwt2"));
         Assert.assertTrue(request.getPreAuthorizedCredentials().containsKey("GeantIncubatorSdJwt2"));
         Assert.assertEquals(
-                request.getPreAuthorizedCredentials().get("GeantIncubatorSdJwt2").getRequestedCredential().size(), 4);
-        Assert.assertEquals(request.getPreAuthorizedCredentials().get("GeantIncubatorSdJwt2").getRequestedCredential()
+                request.getPreAuthorizedCredentials().get("GeantIncubatorSdJwt2").get(0).getContent().getRequestedCredential().size(), 4);
+        Assert.assertEquals(request.getPreAuthorizedCredentials().get("GeantIncubatorSdJwt2").get(0).getContent().getRequestedCredential()
                 .get(1).getPath().get(1), "eppn");
         Assert.assertEquals(request.getCredentialOfferTxCode().getValue(), "12345");
     }
@@ -61,7 +61,7 @@ public class CredentialOfferRequestTest {
     @Test
     public void testNonAuthSuccess() throws ParseException {
         httpRequest.setAuthorization(null);
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"client_id\":\"test\",\"preAuthorizedCredentials\":{\"GeantIncubatorSdJwt2\":[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"edu\",\"eppn\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"givenName\"],\"value\":\"Gemma Gemina Erasmus\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]}}");
         request = CredentialOfferRequest.parse(httpRequest);
         Assert.assertEquals(request.getClientID().toString(), "test");
@@ -77,20 +77,20 @@ public class CredentialOfferRequestTest {
 
     @Test(expectedExceptions = ParseException.class)
     public void testNoCredentialOfferRequestFailure() throws ParseException {
-        httpRequest.setQuery(null);
+        httpRequest.setBody(null);
         request = CredentialOfferRequest.parse(httpRequest);
     }
 
     @Test(expectedExceptions = ParseException.class)
     public void testBothCredentialOfferRequestFailure() throws ParseException {
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"codeCredentials\":[\"GeantIncubatorSdJwt1\",\"GeantIncubatorSdJwt2\"], \"preAuthorizedCredentials\":{\"GeantIncubatorSdJwt2\":[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"edu\",\"eppn\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"givenName\"],\"value\":\"Gemma Gemina Erasmus\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]}}");
         request = CredentialOfferRequest.parse(httpRequest);
     }
 
     @Test(expectedExceptions = ParseException.class)
     public void testCodeFlowRejected() throws ParseException {
-        httpRequest.setQuery("{\"codeCredentials\":[\"GeantIncubatorSdJwt1\",\"GeantIncubatorSdJwt2\"]}");
+        httpRequest.setBody("{\"codeCredentials\":[\"GeantIncubatorSdJwt1\",\"GeantIncubatorSdJwt2\"]}");
         request = CredentialOfferRequest.parse(httpRequest);
     }
 
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequestTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequestTest.java
index 309a184..589eb00 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequestTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCICredentialsRequestTest.java
@@ -36,7 +36,7 @@ public class OpenIDVCICredentialsRequestTest {
         httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Bearer dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proof\": {\n" + "    \"proof_type\": \"jwt\",\n"
                 + "    \"jwt\": \"eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ\"\n"
                 + "  }\n" + "}\n" + "");
@@ -53,7 +53,7 @@ public class OpenIDVCICredentialsRequestTest {
 
     @Test
     public void success2() throws ParseException {
-        httpRequest.setQuery("{\n" + "  \"credential_identifier\": \"CivilEngineeringDegree-2023\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_identifier\": \"CivilEngineeringDegree-2023\",\n"
                 + "  \"proofs\": {\n" + "    \"jwt\": [\n"
                 + "      \"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiblVXQW9BdjNYWml0aDhFN2kxOU9kYXhPTFlGT3dNLVoyRXVNMDJUaXJUNCIsInkiOiJIc2tIVThCalVpMVU5WHFpN1N3bWo4Z3dBS18weGtjRGpFV183MVNvc0VZIn19\",\n"
                 + "      \"eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ\"\n"
@@ -67,7 +67,7 @@ public class OpenIDVCICredentialsRequestTest {
     
     @Test(expectedExceptions = IllegalArgumentException.class)
     public void failProofAndProofs() throws ParseException {
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proof\": {\n" + "    \"proof_type\": \"jwt\",\n"
                 + "    \"jwt\": \"eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ\"\n"
                 + "  }\n" + "  \"proofs\": {\n" + "    \"jwt\": [\n"
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequestTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequestTest.java
index be5cdb8..18f3d8e 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequestTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequestTest.java
@@ -47,7 +47,7 @@ public class OpenIDVCITokenRequestTest {
     protected void setUp() throws Exception {
         httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setContentType(ContentType.APPLICATION_URLENCODED.toString());
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "client_id=clientID&grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code&pre-authorized_code=123456&tx_code=txCodeValue&code_verifier=wzYAXaFZTuabNSWQUDPZuvAzmwVETLFyNJmtJGDWheU&authorization_details=[{\"type\":\"openid_credential\",\"credential_configuration_id\":\"UniversityDegreeCredential\",\"claims\":[{\"path\":[\"credentialSubject\",\"given_name\"],\"mandatory\":true},{\"path\":[\"credentialSubject\",\"family_name\"]},{\"path\":[\"credentialSubject\",\"de [...]
 
         message = OpenIDVCITokenRequest.parse(httpRequest);
@@ -116,7 +116,7 @@ public class OpenIDVCITokenRequestTest {
     public void parseCodeGrant() throws ParseException, JsonProcessingException, URISyntaxException {
         httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setContentType(ContentType.APPLICATION_URLENCODED.toString());
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "client_id=clientID&client_secret=12234&grant_type=authorization_code&code=123456&code_verifier=wzYAXaFZTuabNSWQUDPZuvAzmwVETLFyNJmtJGDWheU&authorization_details=[{\"type\":\"openid_credential\",\"credential_configuration_id\":\"UniversityDegreeCredential\",\"claims\":[{\"path\":[\"credentialSubject\",\"given_name\"],\"mandatory\":true},{\"path\":[\"credentialSubject\",\"family_name\"]},{\"path\":[\"credentialSubject\",\"degree\"]}]}]");
         message = OpenIDVCITokenRequest.parse(httpRequest);
         Assert.assertTrue(message.getAuthorizationGrant() instanceof AuthorizationCodeGrant);
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FilterPotentialCredentialsTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FilterPotentialCredentialsTest.java
new file mode 100644
index 0000000..da75128
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FilterPotentialCredentialsTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.profile.impl;
+
+import java.util.List;
+
+import org.geant.shibboleth.plugin.openidvci.credential.impl.FilesystemCredentialConfigurationsResolver;
+import org.geant.shibboleth.plugin.openidvci.messaging.context.TokenContext;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCIAuthorizationDetail;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+
+/**
+ * Unit tests for {@link FilterPotentialCredentials}.
+ */
+public class FilterPotentialCredentialsTest {
+
+    private static final String CONTENT = "[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"}]";
+
+    private RequestContext requestCtx;
+
+    private TokenContext tokenContext;
+
+    private OIDCAuthenticationResponseContext responseCtx;
+
+    private FilesystemCredentialConfigurationsResolver resolver;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        requestCtx = new RequestContextBuilder().buildRequestContext();
+        final ProfileRequestContext profileRequestCtx =
+                new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+        profileRequestCtx.setInboundMessageContext(new MessageContext());
+        profileRequestCtx.setOutboundMessageContext(new MessageContext());
+        responseCtx = profileRequestCtx.getOutboundMessageContext()
+                .ensureSubcontext(OIDCAuthenticationResponseContext.class);
+        tokenContext = profileRequestCtx.getInboundMessageContext().ensureSubcontext(TokenContext.class);
+        final ClaimsSet potentialCredentials = new ClaimsSet();
+        potentialCredentials.setClaim("ScopedCredential", CONTENT);
+        potentialCredentials.setClaim("OtherCredential", CONTENT);
+        potentialCredentials.setClaim("UnscopedCredential", CONTENT);
+        tokenContext.setPotentialCredentials(potentialCredentials);
+        resolver = new FilesystemCredentialConfigurationsResolver(
+                new ClassPathResource("conf/verifiable-credentials-scoped.json"));
+        resolver.setId("test");
+        resolver.initialize();
+    }
+
+    private FilterPotentialCredentials actionOf(final List<OpenIDVCIAuthorizationDetail> details) throws Exception {
+        final FilterPotentialCredentials action = new FilterPotentialCredentials();
+        action.setCredentialConfigurationsResolver(resolver);
+        action.setAuthorizationDetailsLookupStrategy(prc -> details);
+        action.initialize();
+        return action;
+    }
+
+    @Test
+    public void testScope() throws Exception {
+        responseCtx.setScope(new Scope("openid", "ScopedCredential"));
+        ActionTestingSupport.assertProceedEvent(actionOf(null).execute(requestCtx));
+        Assert.assertEquals(tokenContext.getPotentialCredentials().toJSONObject().keySet(),
+                java.util.Set.of("ScopedCredential"));
+    }
+
+    @Test
+    public void testAuthorizationDetails() throws Exception {
+        ActionTestingSupport.assertProceedEvent(actionOf(List.of(new OpenIDVCIAuthorizationDetail(
+                "openid_credential", "UnscopedCredential", null, null))).execute(requestCtx));
+        Assert.assertEquals(tokenContext.getPotentialCredentials().toJSONObject().keySet(),
+                java.util.Set.of("UnscopedCredential"));
+    }
+
+    @Test
+    public void testScopeAndAuthorizationDetails() throws Exception {
+        responseCtx.setScope(new Scope("OtherCredential"));
+        ActionTestingSupport.assertProceedEvent(actionOf(List.of(new OpenIDVCIAuthorizationDetail(
+                "openid_credential", "ScopedCredential", null, null))).execute(requestCtx));
+        Assert.assertEquals(tokenContext.getPotentialCredentials().toJSONObject().keySet(),
+                java.util.Set.of("ScopedCredential", "OtherCredential"));
+    }
+
+    @Test
+    public void testNothingAuthorized() throws Exception {
+        responseCtx.setScope(new Scope("openid"));
+        ActionTestingSupport.assertProceedEvent(actionOf(null).execute(requestCtx));
+        Assert.assertTrue(tokenContext.getPotentialCredentials().toJSONObject().isEmpty());
+    }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java
index 90b9cc3..aa76d34 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProofTest.java
@@ -61,7 +61,7 @@ public class ParseProofTest {
         httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Bearer dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proofs\": {\n"
                 + "    \"jwt\": [\"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVTMjU2Iiwia2lkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0luVnpaU0k2SW5OcFp5SXNJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVWtGQ2RVaHNTR05IYkdwMlVWVkpkVTlyYTNKQlJEaHJYM2RUVFZSaGNuQnJOR0p5TnpaQldXUTJheUlzSW5raU9pSldYMEZNVERKamFEaEJWMTlHV0hKMVMxWnVZazV0U3poUmVFUnpNblE0UmpSdU5USlFhRWwxVjFObkluMCMwIn0.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0N [...]
                 + "              \"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVTMjU2Iiwia2lkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0luVnpaU0k2SW5OcFp5SXNJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVWtGQ2RVaHNTR05IYkdwMlVWVkpkVTlyYTNKQlJEaHJYM2RUVFZSaGNuQnJOR0p5TnpaQldXUTJheUlzSW5raU9pSldYMEZNVERKamFEaEJWMTlHV0hKMVMxWnVZazV0U3poUmVFUnpNblE0UmpSdU5USlFhRWwxVjFObkluMCMwIn0.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0N [...]
@@ -96,7 +96,7 @@ public class ParseProofTest {
 
     @Test
     public void testUnsupportedProofs() throws ParseException {
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proofs\": {\n"
                 + "    \"ldp_vp\": [\"eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ\"]\n"
                 + "  }\n" + "}\n" + "");
@@ -106,7 +106,7 @@ public class ParseProofTest {
     
     @Test
     public void testAlgNone() throws ParseException {
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proofs\": {\n"
                 + "    \"jwt\": [\"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6Im5vbmUiLCJraWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lVa0ZDZFVoc1NHTkhiR3AyVVZWSmRVOXJhM0pCUkRoclgzZFRUVlJoY25Cck5HSnlOelpCV1dRMmF5SXNJbmtpT2lKV1gwRk1UREpqYURoQlYxOUdXSEoxUzFadVlrNXRTemhSZUVSek1uUTRSalJ1TlRKUWFFbDFWMU5uSW4wIzAifQ.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0ND [...]
                 + "  }\n" + "}\n" + "");
@@ -117,7 +117,7 @@ public class ParseProofTest {
     @Test
     public void testAlgHMAC() throws ParseException {
         
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proofs\": {\n"
                 + "    \"jwt\": [\"eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkhTMjU2Iiwia2lkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmlJc0luVnpaU0k2SW5OcFp5SXNJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVWtGQ2RVaHNTR05IYkdwMlVWVkpkVTlyYTNKQlJEaHJYM2RUVFZSaGNuQnJOR0p5TnpaQldXUTJheUlzSW5raU9pSldYMEZNVERKamFEaEJWMTlHV0hKMVMxWnVZazV0U3poUmVFUnpNblE0UmpSdU5USlFhRWwxVjFObkluMCMwIn0.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0N [...]
                 + "  }\n" + "}\n" + "");
@@ -128,7 +128,7 @@ public class ParseProofTest {
     @Test
     public void testWrongType() throws ParseException {
         
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proofs\": {\n"
                 + "    \"jwt\": [\"eyJ0eXAiOiJzb21lZDR2Y2ktcHJvb2Yrand0IiwiYWxnIjoiRVMyNTYiLCJraWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lVa0ZDZFVoc1NHTkhiR3AyVVZWSmRVOXJhM0pCUkRoclgzZFRUVlJoY25Cck5HSnlOelpCV1dRMmF5SXNJbmtpT2lKV1gwRk1UREpqYURoQlYxOUdXSEoxUzFadVlrNXRTemhSZUVSek1uUTRSalJ1TlRKUWFFbDFWMU5uSW4wIzAifQ.eyJhdWQiOiJodHRwczovL2dlYW50LXZjaS4yLnJhaHRpYXBwLmZpL29pZDR2Y2kiLCJpYXQiOjE3NDQyNzU1OTEsImV4cCI6MTc0ND [...]
                 + "  }\n" + "}\n" + "");
@@ -138,7 +138,7 @@ public class ParseProofTest {
     
     @Test
     public void testMissingProofsWhenConfigurationRequiresThem() throws Exception {
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\"\n" + "}\n" + "");
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\"\n" + "}\n" + "");
         profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
         profileRequestCtx.getInboundMessageContext().ensureSubcontext(CredentialsContext.class)
                 .setCredentialConfiguration(new ObjectMapper()
@@ -150,7 +150,7 @@ public class ParseProofTest {
 
     @Test
     public void testMissingProofsWhenConfigurationDoesNotRequireThem() throws Exception {
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\"\n" + "}\n" + "");
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\"\n" + "}\n" + "");
         profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
         profileRequestCtx.getInboundMessageContext().ensureSubcontext(CredentialsContext.class)
                 .setCredentialConfiguration(
@@ -182,7 +182,7 @@ public class ParseProofTest {
 
     @Test
     public void testEmptyProofs() throws ParseException {
-        httpRequest.setQuery("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
+        httpRequest.setBody("{\n" + "  \"credential_configuration_id\": \"org.iso.18013.5.1.mDL\",\n"
                 + "  \"proofs\": {\n" + "    \"jwt\": []\n" + "  }\n" + "}\n" + "");
         profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetAuthorizationDetailsToResponseContextTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetAuthorizationDetailsToResponseContextTest.java
new file mode 100644
index 0000000..c468ef4
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetAuthorizationDetailsToResponseContextTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.profile.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.context.TokenContext;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCIAuthorizationDetail;
+import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
+import org.geant.shibboleth.plugin.openidvci.profile.logic.OpenIDVCITokenManipulationStrategy;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+
+/**
+ * Unit tests for {@link SetAuthorizationDetailsToResponseContext}.
+ */
+public class SetAuthorizationDetailsToResponseContextTest {
+
+    private static final String CONTENT = "[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"}]";
+
+    private static final String DATASETS = "[{\"dataset_id\":\"bsc\",\"claims\":" + CONTENT + "},"
+            + "{\"dataset_id\":\"msc\",\"claims\":" + CONTENT + "}]";
+
+    private RequestContext requestCtx;
+
+    private TokenContext tokenContext;
+
+    private ClaimsSet potentialCredentials;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        requestCtx = new RequestContextBuilder().buildRequestContext();
+        final ProfileRequestContext profileRequestCtx =
+                new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+        profileRequestCtx.setInboundMessageContext(new MessageContext());
+        tokenContext = profileRequestCtx.getInboundMessageContext().ensureSubcontext(TokenContext.class);
+        potentialCredentials = new ClaimsSet();
+        tokenContext.setPotentialCredentials(potentialCredentials);
+    }
+
+    private SetAuthorizationDetailsToResponseContext actionOf(final List<OpenIDVCIAuthorizationDetail> details)
+            throws Exception {
+        final SetAuthorizationDetailsToResponseContext action = new SetAuthorizationDetailsToResponseContext();
+        action.setAuthorizationDetailsLookupStrategy(prc -> details);
+        action.initialize();
+        return action;
+    }
+
+    private static List<OpenIDVCIAuthorizationDetail> requested(final String configurationId) {
+        return List.of(new OpenIDVCIAuthorizationDetail("openid_credential", configurationId, null, null));
+    }
+
+    @Test
+    public void testNotRequested() throws Exception {
+        potentialCredentials.setClaim("GeantIncubatorDiploma", CONTENT);
+        ActionTestingSupport.assertProceedEvent(actionOf(null).execute(requestCtx));
+        Assert.assertNull(tokenContext.getResponseAuthorizationDetails());
+    }
+
+    @Test
+    public void testRequested() throws Exception {
+        potentialCredentials.setClaim("GeantIncubatorDiploma", CONTENT);
+        ActionTestingSupport.assertProceedEvent(actionOf(requested("GeantIncubatorDiploma")).execute(requestCtx));
+        final List<OpenIDVCIAuthorizationDetail> details = tokenContext.getResponseAuthorizationDetails();
+        Assert.assertNotNull(details);
+        Assert.assertEquals(details.get(0).getCredentialIdentifiers(), List.of("GeantIncubatorDiploma"));
+    }
+
+    @Test
+    public void testRequestedSeveral() throws Exception {
+        potentialCredentials.setClaim("GeantIncubatorDiploma", DATASETS);
+        ActionTestingSupport.assertProceedEvent(actionOf(requested("GeantIncubatorDiploma")).execute(requestCtx));
+        final List<OpenIDVCIAuthorizationDetail> details = tokenContext.getResponseAuthorizationDetails();
+        Assert.assertNotNull(details);
+        Assert.assertEquals(details.get(0).getCredentialIdentifiers(),
+                List.of("GeantIncubatorDiploma#bsc", "GeantIncubatorDiploma#msc"));
+        Assert.assertEquals(tokenContext.getIssuedCredentialIdentifiers().get("GeantIncubatorDiploma#msc"),
+                Map.of(OpenIDVCITokenManipulationStrategy.CREDENTIAL_CONFIGURATION_ID, "GeantIncubatorDiploma",
+                        CredentialDataset.DATASET_ID, "msc"));
+    }
+
+    @Test
+    public void testSeveralNotRequested() throws Exception {
+        potentialCredentials.setClaim("GeantIncubatorDiploma", DATASETS);
+        potentialCredentials.setClaim("GeantIncubatorDiploma2", CONTENT);
+        ActionTestingSupport.assertProceedEvent(actionOf(null).execute(requestCtx));
+        final List<OpenIDVCIAuthorizationDetail> details = tokenContext.getResponseAuthorizationDetails();
+        Assert.assertNotNull(details);
+        Assert.assertEquals(details.size(), 1);
+        Assert.assertEquals(details.get(0).getCredentialConfigurationId(), "GeantIncubatorDiploma");
+    }
+
+    @Test
+    public void testRequestedNotAvailable() throws Exception {
+        potentialCredentials.setClaim("GeantIncubatorDiploma", CONTENT);
+        ActionTestingSupport.assertEvent(actionOf(requested("GeantIncubatorDiploma2")).execute(requestCtx),
+                OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
+    }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetDPoPProofThumbprintToResponseContextTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetDPoPProofThumbprintToResponseContextTest.java
new file mode 100644
index 0000000..14f1a7b
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetDPoPProofThumbprintToResponseContextTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.profile.impl;
+
+import org.geant.shibboleth.plugin.openidvci.decoding.impl.OpenIDVCITokenRequestDecoder;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Unit tests for {@link SetDPoPProofThumbprintToResponseContext}.
+ */
+public class SetDPoPProofThumbprintToResponseContextTest {
+
+    private RequestContext requestCtx;
+
+    private OIDCAuthenticationResponseContext responseCtx;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        final MockHttpServletRequest httpRequest = new MockHttpServletRequest();
+        httpRequest.setMethod("POST");
+        httpRequest.addHeader("Content-Type", "application/x-www-form-urlencoded");
+        httpRequest.addParameter("client_id", "123456");
+        httpRequest.addParameter("pre-authorized_code", "123456");
+        httpRequest.addParameter("grant_type", "urn:ietf:params:oauth:grant-type:pre-authorized_code");
+        final OpenIDVCITokenRequestDecoder decoder = new OpenIDVCITokenRequestDecoder();
+        decoder.setHttpServletRequestSupplier(new NonnullSupplier<>() {
+            public HttpServletRequest get() {
+                return httpRequest;
+            }
+        });
+        decoder.initialize();
+        decoder.decode();
+
+        requestCtx = new RequestContextBuilder().buildRequestContext();
+        final ProfileRequestContext profileRequestCtx =
+                new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+        profileRequestCtx.setInboundMessageContext(decoder.getMessageContext());
+        profileRequestCtx.setOutboundMessageContext(new MessageContext());
+        responseCtx = profileRequestCtx.getOutboundMessageContext()
+                .ensureSubcontext(OIDCAuthenticationResponseContext.class);
+    }
+
+    @Test
+    public void testBound() throws Exception {
+        final SetDPoPProofThumbprintToResponseContext action = new SetDPoPProofThumbprintToResponseContext();
+        action.setDpopProofThumbprintLookupStrategy(prc -> "thumbprint");
+        action.initialize();
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        Assert.assertEquals(responseCtx.getDpopProofJwkThumbprint(), "thumbprint");
+    }
+
+    @Test
+    public void testNoProof() throws Exception {
+        final SetDPoPProofThumbprintToResponseContext action = new SetDPoPProofThumbprintToResponseContext();
+        action.setDpopProofThumbprintLookupStrategy(prc -> null);
+        action.initialize();
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        Assert.assertNull(responseCtx.getDpopProofJwkThumbprint());
+    }
+
+    @Test
+    public void testDefaultLookupNoProof() throws Exception {
+        final SetDPoPProofThumbprintToResponseContext action = new SetDPoPProofThumbprintToResponseContext();
+        action.initialize();
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        Assert.assertNull(responseCtx.getDpopProofJwkThumbprint());
+    }
+
+}
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 0c1f411..853118a 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
@@ -58,7 +58,7 @@ public class ValidateCredentialOfferingTest {
         HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"preAuthorizedCredentials\":{\"GeantIncubatorDiploma\":[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"edu\",\"eppn\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"givenName\"],\"value\":\"Gemma Gemina Erasmus\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]}}");
 
         requestCtx = new RequestContextBuilder().buildRequestContext();
@@ -126,18 +126,51 @@ public class ValidateCredentialOfferingTest {
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
         // Request omits the mandatory "mail" claim declared in the configuration.
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"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 testSuccessDatasets() throws Exception {
+        final String claims = "[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},"
+                + "{\"path\":[\"givenName\"],\"value\":\"Gemma\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]";
+        HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setBody("{\"preAuthorizedCredentials\":{\"GeantIncubatorDiploma\":["
+                + "{\"dataset_id\":\"a\",\"claims\":" + claims + "},"
+                + "{\"dataset_id\":\"b\",\"claims\":" + claims + "}]}}");
+        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");
+        org.testng.Assert.assertEquals(ctx.getValidatedPreAuthorizedCredentials().get("GeantIncubatorDiploma").size(),
+                2);
+    }
+
+    @Test
+    public void testDatasetMissingMandatoryClaim() throws Exception {
+        final String claims = "[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},"
+                + "{\"path\":[\"givenName\"],\"value\":\"Gemma\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]";
+        HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setBody("{\"preAuthorizedCredentials\":{\"GeantIncubatorDiploma\":["
+                + "{\"dataset_id\":\"a\",\"claims\":" + claims + "},"
+                + "{\"dataset_id\":\"b\",\"claims\":[{\"path\":[\"givenName\"],\"value\":\"Gemma\"}]}]}}");
+        profileRequestCtx.getInboundMessageContext().setMessage(CredentialOfferRequest.parse(httpRequest));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIALS_REQUEST);
+    }
+
     @Test(expectedExceptions = ParseException.class)
     public void testCodeFlowRejected() 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\"]}");
+        httpRequest.setBody("{\"codeCredentials\":[\"GeantIncubatorDiploma\"]}");
         CredentialOfferRequest.parse(httpRequest);
     }
 
@@ -146,7 +179,7 @@ public class ValidateCredentialOfferingTest {
         HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"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));
@@ -162,7 +195,7 @@ public class ValidateCredentialOfferingTest {
         HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"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);
@@ -173,7 +206,7 @@ public class ValidateCredentialOfferingTest {
         HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery("{\"codeCredentials\":[\"GeantIncubatorDiploma2\"]}");
+        httpRequest.setBody("{\"codeCredentials\":[\"GeantIncubatorDiploma2\"]}");
         CredentialOfferRequest.parse(httpRequest);
     }
 
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateRequestedCredentialTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateRequestedCredentialTest.java
new file mode 100644
index 0000000..a2d34d1
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateRequestedCredentialTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.profile.impl;
+
+import java.net.URI;
+import java.time.Instant;
+import java.util.Map;
+
+import org.geant.shibboleth.plugin.openidvci.credential.impl.FilesystemCredentialConfigurationsResolver;
+import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialDataset;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCICredentialsRequest;
+import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
+import org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultOpenIDVCICredentialsConfiguration;
+import org.geant.shibboleth.plugin.openidvci.profile.logic.OpenIDVCITokenManipulationStrategy;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.profile.context.RelyingPartyContext;
+
+/**
+ * Unit tests for {@link ValidateRequestedCredential}.
+ */
+public class ValidateRequestedCredentialTest {
+
+    private static final String CONTENT = "[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"}]";
+
+    private static final String DATASETS = "[{\"dataset_id\":\"bsc\",\"claims\":" + CONTENT + "},"
+            + "{\"dataset_id\":\"msc\",\"claims\":" + CONTENT + "}]";
+
+    private RequestContext requestCtx;
+
+    private ProfileRequestContext profileRequestCtx;
+
+    private DefaultOpenIDVCICredentialsConfiguration configuration;
+
+    private ValidateRequestedCredential action;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        requestCtx = new RequestContextBuilder().buildRequestContext();
+        profileRequestCtx = new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+        profileRequestCtx.setInboundMessageContext(new MessageContext());
+        profileRequestCtx.setOutboundMessageContext(new MessageContext());
+        configuration = new DefaultOpenIDVCICredentialsConfiguration();
+        profileRequestCtx.ensureSubcontext(RelyingPartyContext.class).setProfileConfig(configuration);
+        final FilesystemCredentialConfigurationsResolver resolver = new FilesystemCredentialConfigurationsResolver(
+                new ClassPathResource("conf/verifiable-credentials.json"));
+        resolver.setId("test");
+        resolver.initialize();
+        action = new ValidateRequestedCredential();
+        action.setCredentialConfigurationsResolver(resolver);
+        action.initialize();
+    }
+
+    private void setRequest(final String body) throws Exception {
+        final HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Bearer dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setBody(body);
+        profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+    }
+
+    private void setGrant(final Map<String, Object> potentialClaims, final Map<String, Object> identifiers) {
+        final AccessTokenClaimsSet.Builder builder = new AccessTokenClaimsSet.Builder();
+        builder.setJWTID("jti").setClientID(new ClientID("client")).setIssuer("issuer").setPrincipal("principal")
+                .setSubject("subject").setIssuedAt(Instant.now()).setExpiresAt(Instant.now().plusSeconds(60))
+                .setAuthenticationTime(Instant.now());
+        builder.addCustomClaim(OpenIDVCITokenManipulationStrategy.POTENTIAL_CLAIMS, potentialClaims);
+        if (identifiers != null) {
+            builder.addCustomClaim(OpenIDVCITokenManipulationStrategy.CREDENTIAL_IDENTIFIERS, identifiers);
+        }
+        profileRequestCtx.getOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class)
+                .setAuthorizationGrantClaimsSet(builder.build());
+    }
+
+    private static Map<String, Object> issued(final String configurationId) {
+        return Map.of(OpenIDVCITokenManipulationStrategy.CREDENTIAL_CONFIGURATION_ID, configurationId);
+    }
+
+    private static Map<String, Object> issued(final String configurationId, final String datasetId) {
+        return Map.of(OpenIDVCITokenManipulationStrategy.CREDENTIAL_CONFIGURATION_ID, configurationId,
+                CredentialDataset.DATASET_ID, datasetId);
+    }
+
+    @Test
+    public void testConfigurationId() throws Exception {
+        setRequest("{\"credential_configuration_id\":\"GeantIncubatorDiploma\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT), null);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        final CredentialsContext ctx = profileRequestCtx.getInboundMessageContext()
+                .getSubcontext(CredentialsContext.class);
+        Assert.assertNotNull(ctx);
+        Assert.assertEquals(ctx.getCredentialIdentifier(), "GeantIncubatorDiploma");
+        Assert.assertEquals(ctx.getCredentialConfigurationId(), "GeantIncubatorDiploma");
+        Assert.assertNull(ctx.getDatasetId());
+        Assert.assertNotNull(ctx.getCredentialConfiguration());
+        Assert.assertNotNull(ctx.getCredentialContent());
+    }
+
+    @Test
+    public void testUnknownConfigurationId() throws Exception {
+        setRequest("{\"credential_configuration_id\":\"Unknown\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT), null);
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
+    }
+
+    @Test
+    public void testConfigurationIdNotAuthorized() throws Exception {
+        setRequest("{\"credential_configuration_id\":\"GeantIncubatorDiploma2\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT), null);
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIALS_FOR_REQUEST);
+    }
+
+    @Test
+    public void testConfigurationIdWithIssuedIdentifiers() throws Exception {
+        setRequest("{\"credential_configuration_id\":\"GeantIncubatorDiploma\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT),
+                Map.of("GeantIncubatorDiploma", issued("GeantIncubatorDiploma")));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIAL_REQUEST);
+    }
+
+    @Test
+    public void testConfigurationIdWithIssuedIdentifiersAllowed() throws Exception {
+        configuration.setAllowCredentialConfigurationIdWithIdentifiers(true);
+        setRequest("{\"credential_configuration_id\":\"GeantIncubatorDiploma\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT),
+                Map.of("GeantIncubatorDiploma", issued("GeantIncubatorDiploma")));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+    }
+
+    @Test
+    public void testIdentifier() throws Exception {
+        setRequest("{\"credential_identifier\":\"GeantIncubatorDiploma#bsc\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", DATASETS),
+                Map.of("GeantIncubatorDiploma#bsc", issued("GeantIncubatorDiploma", "bsc"),
+                        "GeantIncubatorDiploma#msc", issued("GeantIncubatorDiploma", "msc")));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        final CredentialsContext ctx = profileRequestCtx.getInboundMessageContext()
+                .getSubcontext(CredentialsContext.class);
+        Assert.assertNotNull(ctx);
+        Assert.assertEquals(ctx.getCredentialIdentifier(), "GeantIncubatorDiploma#bsc");
+        Assert.assertEquals(ctx.getCredentialConfigurationId(), "GeantIncubatorDiploma");
+        Assert.assertEquals(ctx.getDatasetId(), "bsc");
+        Assert.assertNotNull(ctx.getCredentialConfiguration());
+    }
+
+    @Test
+    public void testConfigurationIdSeveralDatasets() throws Exception {
+        configuration.setAllowCredentialConfigurationIdWithIdentifiers(true);
+        setRequest("{\"credential_configuration_id\":\"GeantIncubatorDiploma\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", DATASETS),
+                Map.of("GeantIncubatorDiploma#bsc", issued("GeantIncubatorDiploma", "bsc"),
+                        "GeantIncubatorDiploma#msc", issued("GeantIncubatorDiploma", "msc")));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIAL_REQUEST);
+    }
+
+    @Test
+    public void testUnknownIdentifier() throws Exception {
+        setRequest("{\"credential_identifier\":\"unknown:e6d200f6\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT),
+                Map.of("GeantIncubatorDiploma", issued("GeantIncubatorDiploma")));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx),
+                OpenIDVCIEventIds.UNKNOWN_CREDENTIAL_IDENTIFIER);
+    }
+
+    @Test
+    public void testIdentifierNoneIssued() throws Exception {
+        setRequest("{\"credential_identifier\":\"GeantIncubatorDiploma\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT), null);
+        ActionTestingSupport.assertEvent(action.execute(requestCtx),
+                OpenIDVCIEventIds.UNKNOWN_CREDENTIAL_IDENTIFIER);
+    }
+
+    @Test
+    public void testIdentifierWithoutContent() throws Exception {
+        setRequest("{\"credential_identifier\":\"GeantIncubatorDiploma#phd\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", DATASETS),
+                Map.of("GeantIncubatorDiploma#phd", issued("GeantIncubatorDiploma", "phd")));
+        ActionTestingSupport.assertEvent(action.execute(requestCtx),
+                OpenIDVCIEventIds.UNKNOWN_CREDENTIAL_IDENTIFIER);
+    }
+
+    @Test
+    public void testBoth() throws Exception {
+        setRequest("{\"credential_identifier\":\"GeantIncubatorDiploma\","
+                + "\"credential_configuration_id\":\"GeantIncubatorDiploma\"}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT), null);
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIAL_REQUEST);
+    }
+
+    @Test
+    public void testNeither() throws Exception {
+        setRequest("{}");
+        setGrant(Map.of("GeantIncubatorDiploma", CONTENT), null);
+        ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.NO_CREDENTIAL_REQUEST);
+    }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferCacheTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferCacheTest.java
index 234c61b..0422d52 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferCacheTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferCacheTest.java
@@ -55,7 +55,7 @@ public class CredentialOfferCacheTest {
         HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"preAuthorizedCredentials\":{\"GeantIncubatorSdJwt2\":[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"edu\",\"eppn\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"givenName\"],\"value\":\"Gemma Gemina Erasmus\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]}}");
         credentialOfferObject = new CredentialOfferObject(new ClientID("clientID_XYZ"),
                 CredentialOfferRequest.parse(httpRequest).getPreAuthorizedCredentials(),
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObjectTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObjectTest.java
index 85266d1..7b8fdd5 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObjectTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/storage/CredentialOfferObjectTest.java
@@ -43,7 +43,7 @@ public class CredentialOfferObjectTest {
         HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
         httpRequest.setAuthorization("Basic dGVzdDp0ZXN0");
         httpRequest.setContentType("application/json");
-        httpRequest.setQuery(
+        httpRequest.setBody(
                 "{\"preAuthorizedCredentials\":{\"GeantIncubatorSdJwt2\":[{\"path\":[\"mail\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"edu\",\"eppn\"],\"value\":\"incubatorUser at example.org\"},{\"path\":[\"givenName\"],\"value\":\"Gemma Gemina Erasmus\"},{\"path\":[\"familyName\"],\"value\":\"Erasmus\"}]}}");
         object = new CredentialOfferObject(new ClientID("clientID_XYZ"),
                 CredentialOfferRequest.parse(httpRequest).getPreAuthorizedCredentials(),
@@ -53,7 +53,7 @@ public class CredentialOfferObjectTest {
         Assert.assertEquals(parsedObject.getCodeCredentials().size(), 3);
         Assert.assertEquals(parsedObject.getCodeCredentials().get(2), "GeantIncubatorSdJwt3");
         Assert.assertTrue(parsedObject.getPreAuthorizedCredentials().containsKey("GeantIncubatorSdJwt2"));
-        Assert.assertEquals(parsedObject.getPreAuthorizedCredentials().get("GeantIncubatorSdJwt2").getRequestedCredential()
+        Assert.assertEquals(parsedObject.getPreAuthorizedCredentials().get("GeantIncubatorSdJwt2").get(0).getContent().getRequestedCredential()
                 .get(0).getValue(), "incubatorUser at example.org");
         Assert.assertEquals(parsedObject.getCredentialOfferTxCode().getValue(), "12345");
         
diff --git a/openid-vci-impl/src/test/resources/conf/verifiable-credentials-scoped.json b/openid-vci-impl/src/test/resources/conf/verifiable-credentials-scoped.json
new file mode 100644
index 0000000..35b1114
--- /dev/null
+++ b/openid-vci-impl/src/test/resources/conf/verifiable-credentials-scoped.json
@@ -0,0 +1,24 @@
+{
+    "credential_configurations_supported": {
+      "ScopedCredential": {
+        "format": "dc+sd-jwt",
+        "vct": "ScopedCredential",
+        "scope": "ScopedCredential",
+        "cryptographic_binding_methods_supported": [ "did:jwk" ],
+        "proof_types_supported": { "jwt": { "proof_signing_alg_values_supported": [ "ES256" ] } }
+      },
+      "OtherCredential": {
+        "format": "dc+sd-jwt",
+        "vct": "OtherCredential",
+        "scope": "OtherCredential",
+        "cryptographic_binding_methods_supported": [ "did:jwk" ],
+        "proof_types_supported": { "jwt": { "proof_signing_alg_values_supported": [ "ES256" ] } }
+      },
+      "UnscopedCredential": {
+        "format": "dc+sd-jwt",
+        "vct": "UnscopedCredential",
+        "cryptographic_binding_methods_supported": [ "did:jwk" ],
+        "proof_types_supported": { "jwt": { "proof_signing_alg_values_supported": [ "ES256" ] } }
+      }
+    }
+}

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


More information about the commits mailing list