[java-idp-plugin-vci] 01/01: Rework Token endpoint to align with OP Token endpoint implementation. Idea is to merge these in the future to one. Align all endpoints to have standard OP hooks

Codeberg noreply at shibboleth.net
Tue Sep 22 09:15:58 UTC 2026


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

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

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

commit 777d1818198549c9a10e8057189e57c77a0960ce
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Tue Sep 22 12:15:27 2026 +0300

    Rework Token endpoint to align with OP Token endpoint implementation. Idea is to merge these in the future to one. Align all endpoints to have standard OP hooks
---
 README.md                                          |  16 +-
 .../openidvci/messaging/error/OpenIDVCIError.java  |  40 +
 .../openidvci/profile/OpenIDVCIEventIds.java       |   7 +
 .../impl/OpenIDVCITokenRequestDecoder.java         |   4 +-
 .../AuthorizationDetailsLookupFunction.java        |   5 +-
 .../impl/AbstractOpenIDVCITokenRequestAction.java  |   2 +-
 .../CredentialIssuerMetadataSuccessResponse.java   |  10 +
 .../impl/CredentialOfferSuccessResponse.java       |   2 -
 .../messaging/impl/NonceSuccessResponse.java       |  11 +
 .../messaging/impl/OpenIDVCITokenRequest.java      | 128 ++-
 .../messaging/impl/PreAuthorizedCodeGrant.java     | 136 ++++
 ...nedCredentialIssuerMetadataSuccessResponse.java |  10 +
 .../impl/DefaultOpenIDVCITokenConfiguration.java   |  91 ++-
 .../profile/impl/BuildCredentialOfferToken.java    |   3 +-
 .../plugin/openidvci/profile/impl/ParseProof.java  |   5 +
 .../plugin/openidvci/profile/impl/UnwrapGrant.java |  48 +-
 .../openid/vci/abstract-api/abstract-api-flow.xml  |  21 +-
 .../flows/openid/vci/abstract/abstract-flow.xml    |  21 +
 .../openid/vci/credentials/credentials-beans.xml   |  35 +-
 .../idp/flows/openid/vci/token/token-beans.xml     | 893 +++++++++++++++++++--
 .../idp/flows/openid/vci/token/token-flow.xml      | 374 +++++++--
 .../messaging/impl/OpenIDVCITokenRequestTest.java  |  46 +-
 .../DefaultOpenIDVCITokenConfigurationTest.java    |  75 ++
 23 files changed, 1698 insertions(+), 285 deletions(-)

diff --git a/README.md b/README.md
index 76596bb..cc82be6 100644
--- a/README.md
+++ b/README.md
@@ -1681,8 +1681,12 @@ The relying party is the wallet, not the caller that created the offer. Put the
 The wallet forms the request and its parameters are the ones of the specification, so there is
 nothing here for you to call.
 
-The profile has no settings of this plugin. `accessTokenClaimsSetManipulationStrategy` is a
-setting of the OP plugin and it has to name **openidvci.TokenManipulationStrategy**, see
+`grantTypes` decides which grants this endpoint serves. It defaults to `authorization_code` and
+`urn:ietf:params:oauth:grant-type:pre-authorized_code`, and a wallet's own `grant_types` of
+*metadata/oidc-client.json* has to name the one it uses.
+
+`accessTokenClaimsSetManipulationStrategy` is a setting of the OP plugin and it has to name
+**openidvci.TokenManipulationStrategy**, see
 [Pre-authorized code flow](#pre-authorized-code-flow) and
 [Authorization code flow](#authorization-code-flow).
 
@@ -2057,6 +2061,14 @@ settings that are documented for the profile configurations of the OP plugin.
 `tokenEndpointAuthMethods`, `accessTokenLifetime`, `forcePKCE`, `securityConfiguration` and the
 DPoP settings are among them.
 
+#### Interceptor flows and message handlers
+
+| Name | Type | Runs |
+|---|---|---|
+| `inboundInterceptorFlows` | List<String> | After the request is decoded and the profile is selected, before authentication. |
+| `outboundInterceptorFlows` | List<String> | After the response message is formed, before it is committed. Also for an error response. |
+| `messageHandler` | Function<MessageContext,Exception> | On the inbound message context before authentication, and on the outbound one before encoding. |
+
 | Name | Type | Default | Description |
 |---|---|---|---|
 | `preAuthorizedCodeLifetime` | Duration | `PT10M` | Lifetime of the pre-authorized code. |
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/error/OpenIDVCIError.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/error/OpenIDVCIError.java
index cfac17d..6c1b098 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/error/OpenIDVCIError.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/error/OpenIDVCIError.java
@@ -36,6 +36,46 @@ public final class OpenIDVCIError {
     public static final ErrorObject NO_CREDENTIAL_OFFER = new ErrorObject("no_credential_offer",
             "No active credential offer per pre-authorized code", HTTPResponse.SC_BAD_REQUEST);
 
+    /**
+     * The Credential Request is missing a required parameter, includes an unsupported parameter or parameter
+     * value, repeats the same parameter, or is otherwise malformed.
+     */
+    public static final ErrorObject INVALID_CREDENTIAL_REQUEST = new ErrorObject("invalid_credential_request",
+            "The credential request is malformed", HTTPResponse.SC_BAD_REQUEST);
+
+    /** Requested Credential Configuration is unknown. */
+    public static final ErrorObject UNKNOWN_CREDENTIAL_CONFIGURATION = new ErrorObject(
+            "unknown_credential_configuration", "Requested credential configuration is unknown",
+            HTTPResponse.SC_BAD_REQUEST);
+
+    /** Requested Credential identifier is unknown. */
+    public static final ErrorObject UNKNOWN_CREDENTIAL_IDENTIFIER = new ErrorObject(
+            "unknown_credential_identifier", "Requested credential identifier is unknown",
+            HTTPResponse.SC_BAD_REQUEST);
+
+    /**
+     * The proofs parameter of the Credential Request is invalid: the field is missing, or one of the key
+     * proofs is invalid, or one of the key proofs carries no nonce.
+     */
+    public static final ErrorObject INVALID_PROOF = new ErrorObject("invalid_proof",
+            "The key proofs of the request are invalid", HTTPResponse.SC_BAD_REQUEST);
+
+    /** At least one of the key proofs carries an invalid nonce, and the Wallet should fetch a new one. */
+    public static final ErrorObject INVALID_NONCE = new ErrorObject("invalid_nonce",
+            "The nonce of a key proof is invalid", HTTPResponse.SC_BAD_REQUEST);
+
+    /** The encryption parameters of the Credential Request are invalid or missing. */
+    public static final ErrorObject INVALID_ENCRYPTION_PARAMETERS = new ErrorObject(
+            "invalid_encryption_parameters", "The encryption parameters of the request are invalid or missing",
+            HTTPResponse.SC_BAD_REQUEST);
+
+    /**
+     * The Credential Request has not been accepted by the Credential Issuer. The Wallet is to treat this as
+     * unrecoverable, the credential cannot be issued.
+     */
+    public static final ErrorObject CREDENTIAL_REQUEST_DENIED = new ErrorObject("credential_request_denied",
+            "The credential request was not accepted", HTTPResponse.SC_BAD_REQUEST);
+
     /** Constructor. */
     private OpenIDVCIError() {
     }
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 d6e564f..41db26f 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
@@ -85,6 +85,13 @@ public final class OpenIDVCIEventIds {
     @NotEmpty
     public static final String INVALID_PROOF = "InvalidProof";
 
+    /**
+     * Nonce of a key proof is invalid.
+     */
+    @Nonnull
+    @NotEmpty
+    public static final String INVALID_NONCE = "InvalidNonce";
+
     /**
      * Tx code validation failed.
      */
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/OpenIDVCITokenRequestDecoder.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/OpenIDVCITokenRequestDecoder.java
index ce902f4..e74bb39 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/OpenIDVCITokenRequestDecoder.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/decoding/impl/OpenIDVCITokenRequestDecoder.java
@@ -60,8 +60,8 @@ public class OpenIDVCITokenRequestDecoder extends BaseOpenIDVCIRequestDecoder<Op
     protected String getMessageToLog(final OpenIDVCITokenRequest message) {
         String details = null;
         try {
-            details = message.getAuthorizationDetails() != null
-                    ? new ObjectMapper().writeValueAsString(message.getAuthorizationDetails())
+            details = message.getCredentialAuthorizationDetails() != null
+                    ? new ObjectMapper().writeValueAsString(message.getCredentialAuthorizationDetails())
                     : null;
         } catch (final JsonProcessingException e) {
             log.warn("Failed serializing authorization_details", e);
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 76a2a7b..6f8ad27 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
@@ -64,8 +64,9 @@ public class AuthorizationDetailsLookupFunction
             }
         }
         if (input.getInboundMessageContext().getMessage() instanceof OpenIDVCITokenRequest tokenRequest) {
-            if (tokenRequest.getAuthorizationDetails() != null && !tokenRequest.getAuthorizationDetails().isEmpty()) {
-                return tokenRequest.getAuthorizationDetails();
+            if (tokenRequest.getCredentialAuthorizationDetails() != null
+                    && !tokenRequest.getCredentialAuthorizationDetails().isEmpty()) {
+                return tokenRequest.getCredentialAuthorizationDetails();
             }
             final MessageContext msgCtx = input.getOutboundMessageContext();
             if (msgCtx == null) {
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/AbstractOpenIDVCITokenRequestAction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/AbstractOpenIDVCITokenRequestAction.java
index ed14396..7d1c453 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/AbstractOpenIDVCITokenRequestAction.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/AbstractOpenIDVCITokenRequestAction.java
@@ -39,7 +39,7 @@ public abstract class AbstractOpenIDVCITokenRequestAction extends AbstractOIDCRe
      */
     @Nullable
     public TokenRequest getTokenRequest() {
-        return getRequest().getOPTokenRequest();
+        return getRequest();
     }
 
     /**
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
index e60c6f8..dbbe649 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/CredentialIssuerMetadataSuccessResponse.java
@@ -49,6 +49,16 @@ public class CredentialIssuerMetadataSuccessResponse implements SuccessResponse
         content = new ObjectMapper().writeValueAsString(metadata);
     }
 
+    /**
+     * Get the serialized metadata document.
+     *
+     * @return serialized metadata document
+     */
+    @Nonnull
+    public String getContent() {
+        return content;
+    }
+
     /** {@inheritDoc} */
     @Override
     public boolean indicatesSuccess() {
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 485499b..73ce78a 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
@@ -49,8 +49,6 @@ public class CredentialOfferSuccessResponse implements SuccessResponse {
 
     /**
      * Percent-encoder for the Credential Offer query parameter value.
-     * 
-     * RFC 3986 unreserved set plus ":", "/" and ",".
      */
     @Nonnull
     private static final PercentEscaper OFFER_ESCAPER = new PercentEscaper("-._~:/,", false);
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/NonceSuccessResponse.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/NonceSuccessResponse.java
index cc25b5c..56bd6ac 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/NonceSuccessResponse.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/NonceSuccessResponse.java
@@ -59,6 +59,17 @@ public class NonceSuccessResponse implements SuccessResponse {
         this.cNonce = cNonce;
     }
 
+    /**
+     * Get c_nonce.
+     *
+     * @return c_nonce
+     */
+    @Nonnull
+    @NotEmpty
+    public String getCNonce() {
+        return cNonce;
+    }
+
     /** {@inheritDoc} */
     @Override
     public boolean indicatesSuccess() {
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequest.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequest.java
index ba81849..922fbff 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequest.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/OpenIDVCITokenRequest.java
@@ -25,9 +25,10 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonMappingException;
 import com.nimbusds.common.contenttype.ContentType;
-import com.nimbusds.oauth2.sdk.AbstractOptionallyIdentifiedRequest;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.oauth2.sdk.TokenRequest;
@@ -35,6 +36,7 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
 import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest;
 import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
 import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
 import com.nimbusds.oauth2.sdk.util.StringUtils;
 
@@ -42,7 +44,7 @@ import com.nimbusds.oauth2.sdk.util.StringUtils;
  * Class implementing Open ID VCI Token Request message as described in
  * https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-token-request.
  */
-public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
+public class OpenIDVCITokenRequest extends TokenRequest {
 
     /** Grant Type value for pre-auth flow. */
     public static final String GRANT_TYPE_VALUE_PRE_AUTH = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
@@ -84,10 +86,6 @@ public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
     @Nullable
     private final List<OpenIDVCIAuthorizationDetail> authorizationDetails;
 
-    /** Request parsed as message understood by OP Token endpoint. */
-    @Nullable
-    private final TokenRequest opTokenRequest;
-
 // Checkstyle: ParameterNumber OFF
     /**
      * Constructor.
@@ -103,38 +101,18 @@ public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
      * @param txCode            The tx-code in pre-authorized flow
      * @param codeVerifier      The PKCE code verifier
      * @param authorizationDetails Authorization details of the request
-     * @param opTokenRequest    Underlying OP token request
      */
     public OpenIDVCITokenRequest(@Nullable final URI uri, @Nullable final ClientAuthentication clientAuth,
             @Nonnull final String grantType, @Nullable final String preAuthorizedCode, @Nullable final String code,
             @Nullable final String txCode, @Nullable final String codeVerifier,
-            @Nullable final List<OpenIDVCIAuthorizationDetail> authorizationDetails,
-            @Nullable final TokenRequest opTokenRequest) {
-        super(uri, clientAuth);
-        if (GRANT_TYPE_VALUE_PRE_AUTH.equals(grantType)) {
-            if (preAuthorizedCode == null || preAuthorizedCode.isEmpty()) {
-                throw new IllegalArgumentException(
-                        "pre-auth code must not be null or empty for pre-authorized_code grant");
-            }
-        } else if (GRANT_TYPE_VALUE_CODE.equals(grantType)) {
-            if (code == null || code.isEmpty()) {
-                throw new IllegalArgumentException("code must not be null or empty for authorization_code grant");
-            }
-        } else {
-            throw new IllegalArgumentException(
-                    "Grant type must be either 'authorization_code' or "
-                            + "'urn:ietf:params:oauth:grant-type:pre-authorized_code'");
-        }
-        if (codeVerifier != null && codeVerifier.length() < 43) {
-            throw new IllegalArgumentException("The code verifier must be at least 43 characters");
-        }
+            @Nullable final List<OpenIDVCIAuthorizationDetail> authorizationDetails) {
+        super(uri, clientAuth, toAuthorizationGrant(grantType, preAuthorizedCode, code, txCode, codeVerifier));
         this.grantType = grantType;
         this.preAuthorizedCode = preAuthorizedCode;
         this.code = code;
         this.txCode = txCode;
         this.codeVerifier = codeVerifier;
         this.authorizationDetails = authorizationDetails;
-        this.opTokenRequest = opTokenRequest;
     }
 
     /**
@@ -152,39 +130,20 @@ public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
      * @param txCode            The tx-code in pre-authorized flow
      * @param codeVerifier      The PKCE code verifier
      * @param authorizationDetails Authorization details of the request
-     * @param opTokenRequest    Underlying OP token request
      */
     public OpenIDVCITokenRequest(@Nullable final URI uri, @Nullable final ClientID clientID,
             @Nonnull final String grantType,
             @Nullable final String preAuthorizedCode, @Nullable final String code, @Nullable final String txCode,
             @Nullable final String codeVerifier,
-            @Nullable final List<OpenIDVCIAuthorizationDetail> authorizationDetails,
-            @Nullable final TokenRequest opTokenRequest) {
-        super(uri, (ClientID) clientID);
-        if (GRANT_TYPE_VALUE_PRE_AUTH.equals(grantType)) {
-            if (preAuthorizedCode == null || preAuthorizedCode.isEmpty()) {
-                throw new IllegalArgumentException(
-                        "pre-auth code must not be null or empty for pre-authorized_code grant");
-            }
-        } else if (GRANT_TYPE_VALUE_CODE.equals(grantType)) {
-            if (code == null || code.isEmpty()) {
-                throw new IllegalArgumentException("code must not be null or empty for authorization_code grant");
-            }
-        } else {
-            throw new IllegalArgumentException(
-                    "Grant type must be either 'authorization_code' or "
-                            + "'urn:ietf:params:oauth:grant-type:pre-authorized_code'");
-        }
-        if (codeVerifier != null && codeVerifier.length() < 43) {
-            throw new IllegalArgumentException("The code verifier must be at least 43 characters");
-        }
+            @Nullable final List<OpenIDVCIAuthorizationDetail> authorizationDetails) {
+        super(uri, (ClientID) clientID, toAuthorizationGrant(grantType, preAuthorizedCode, code, txCode,
+                codeVerifier));
         this.grantType = grantType;
         this.preAuthorizedCode = preAuthorizedCode;
         this.code = code;
         this.txCode = txCode;
         this.codeVerifier = codeVerifier;
         this.authorizationDetails = authorizationDetails;
-        this.opTokenRequest = opTokenRequest;
     }
 // Checkstyle: ParameterNumber ON
 
@@ -239,22 +198,50 @@ public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
     }
 
     /**
-     * Get Authorization details.
+     * Get openid_credential authorization details of the request.
      * 
-     * @return Authorization details
+     * @return openid_credential authorization details of the request
      */
     @Nullable
-    public List<OpenIDVCIAuthorizationDetail> getAuthorizationDetails() {
+    public List<OpenIDVCIAuthorizationDetail> getCredentialAuthorizationDetails() {
         return authorizationDetails;
     }
 
     /**
-     * Get request parsed as message understood by OP Token endpoint.
-     * 
-     * @return Request parsed as message understood by OP Token endpoint
+     * Form the authorization grant of the request.
+     *
+     * @param grantType         grant type of the request
+     * @param preAuthorizedCode pre-authorized code, or null
+     * @param code              authorization code, or null
+     * @param txCode            transaction code, or null
+     * @param codeVerifier      PKCE code verifier, or null
+     *
+     * @return the grant
      */
-    public TokenRequest getOPTokenRequest() {
-        return opTokenRequest;
+    @Nonnull
+    private static AuthorizationGrant toAuthorizationGrant(@Nonnull final String grantType,
+            @Nullable final String preAuthorizedCode, @Nullable final String code, @Nullable final String txCode,
+            @Nullable final String codeVerifier) {
+
+        if (codeVerifier != null && codeVerifier.length() < 43) {
+            throw new IllegalArgumentException("The code verifier must be at least 43 characters");
+        }
+        if (GRANT_TYPE_VALUE_PRE_AUTH.equals(grantType)) {
+            if (preAuthorizedCode == null || preAuthorizedCode.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "pre-auth code must not be null or empty for pre-authorized_code grant");
+            }
+            return new PreAuthorizedCodeGrant(preAuthorizedCode, txCode);
+        }
+        if (GRANT_TYPE_VALUE_CODE.equals(grantType)) {
+            if (code == null || code.isEmpty()) {
+                throw new IllegalArgumentException("code must not be null or empty for authorization_code grant");
+            }
+            return new AuthorizationCodeGrant(new AuthorizationCode(code), null,
+                    codeVerifier != null ? new CodeVerifier(codeVerifier) : null);
+        }
+        throw new IllegalArgumentException("Grant type must be either 'authorization_code' or "
+                + "'urn:ietf:params:oauth:grant-type:pre-authorized_code'");
     }
 
     /**
@@ -270,12 +257,9 @@ public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
      * 
      * @param httpRequest request to parse.
      * @return parsed request.
-     * @throws ParseException          if parsing failed.
-     * @throws JsonProcessingException
-     * @throws JsonMappingException
+     * @throws ParseException if parsing failed.
      */
-    public static OpenIDVCITokenRequest parse(final HTTPRequest httpRequest)
-            throws ParseException, JsonProcessingException {
+    public static OpenIDVCITokenRequest parse(final HTTPRequest httpRequest) throws ParseException {
         httpRequest.ensureMethod(HTTPRequest.Method.POST);
         httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
         final ClientAuthentication clientAuth;
@@ -298,12 +282,12 @@ public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
         final String code = MultivaluedMapUtils.getFirstValue(params, "code");
         final String txCode = MultivaluedMapUtils.getFirstValue(params, "tx_code");
         final String codeVerifier = MultivaluedMapUtils.getFirstValue(params, "code_verifier");
-        final List<OpenIDVCIAuthorizationDetail> authorizationDetails = OpenIDVCIAuthorizationDetail.parse(httpRequest);
-        TokenRequest opTokenRequest = null;
+        final List<OpenIDVCIAuthorizationDetail> authorizationDetails;
         try {
-            opTokenRequest = TokenRequest.parse(httpRequest);
-        } catch (final ParseException e) {
-            // no-op. pre-authorize grant is not parsable.
+            authorizationDetails = OpenIDVCIAuthorizationDetail.parse(httpRequest);
+        } catch (final JsonProcessingException e) {
+            throw new ParseException(e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
         }
         final URI uri;
         try {
@@ -313,15 +297,15 @@ public class OpenIDVCITokenRequest extends AbstractOptionallyIdentifiedRequest {
         }
         if (clientAuth != null) {
             return new OpenIDVCITokenRequest(uri, clientAuth, grantType, preAuthorizedCode, code, txCode, codeVerifier,
-                    authorizationDetails, opTokenRequest);
+                    authorizationDetails);
         }
         final String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
         if (StringUtils.isBlank(clientIDString)) {
             return new OpenIDVCITokenRequest(uri, (ClientID) null, grantType, preAuthorizedCode, code, txCode,
-                    codeVerifier, authorizationDetails, opTokenRequest);
+                    codeVerifier, authorizationDetails);
         }
         return new OpenIDVCITokenRequest(uri, new ClientID(clientIDString), grantType, preAuthorizedCode, code, txCode,
-                codeVerifier, authorizationDetails, opTokenRequest);
+                codeVerifier, authorizationDetails);
     }
 
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/PreAuthorizedCodeGrant.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/PreAuthorizedCodeGrant.java
new file mode 100644
index 0000000..11da4b7
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/PreAuthorizedCodeGrant.java
@@ -0,0 +1,136 @@
+/*
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
+import com.nimbusds.oauth2.sdk.util.StringUtils;
+
+/**
+ * The pre-authorized code grant of OpenID for Verifiable Credential Issuance, which the Nimbus SDK does not
+ * implement.
+ */
+public class PreAuthorizedCodeGrant extends AuthorizationGrant {
+
+    /** Grant type of the pre-authorized code grant. */
+    @Nonnull
+    public static final GrantType GRANT_TYPE = new GrantType(OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH);
+
+    /** Parameter carrying the pre-authorized code. */
+    @Nonnull
+    public static final String PRE_AUTHORIZED_CODE_PARAM = "pre-authorized_code";
+
+    /** Parameter carrying the transaction code. */
+    @Nonnull
+    public static final String TX_CODE_PARAM = "tx_code";
+
+    /** The pre-authorized code of the credential offer. */
+    @Nonnull
+    private final String preAuthorizedCode;
+
+    /** Transaction code the offer asked the Wallet to present, or null. */
+    @Nullable
+    private final String txCode;
+
+    /**
+     * Constructor.
+     *
+     * @param code    pre-authorized code of the credential offer
+     * @param code2   transaction code the offer asked the Wallet to present, or null
+     */
+    public PreAuthorizedCodeGrant(@Nonnull final String code, @Nullable final String code2) {
+        super(GRANT_TYPE);
+        preAuthorizedCode = code;
+        txCode = code2;
+    }
+
+    /**
+     * Get the pre-authorized code of the credential offer.
+     *
+     * @return pre-authorized code of the credential offer
+     */
+    @Nonnull
+    public String getPreAuthorizedCode() {
+        return preAuthorizedCode;
+    }
+
+    /**
+     * Get the transaction code the offer asked the Wallet to present.
+     *
+     * @return transaction code, or null
+     */
+    @Nullable
+    public String getTxCode() {
+        return txCode;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Map<String, List<String>> toParameters() {
+        final Map<String, List<String>> params = new HashMap<>();
+        final List<String> types = new ArrayList<>();
+        types.add(GRANT_TYPE.getValue());
+        params.put("grant_type", types);
+        final List<String> codes = new ArrayList<>();
+        codes.add(preAuthorizedCode);
+        params.put(PRE_AUTHORIZED_CODE_PARAM, codes);
+        if (txCode != null) {
+            final List<String> txCodes = new ArrayList<>();
+            txCodes.add(txCode);
+            params.put(TX_CODE_PARAM, txCodes);
+        }
+        return params;
+    }
+
+    /**
+     * Parse the grant from the parameters of a token request.
+     *
+     * @param params parameters of the token request
+     *
+     * @return the parsed grant
+     *
+     * @throws ParseException if the parameters name no pre-authorized code grant
+     */
+    @Nonnull
+    public static PreAuthorizedCodeGrant parse(@Nonnull final Map<String, List<String>> params)
+            throws ParseException {
+
+        final String grantType = MultivaluedMapUtils.getFirstValue(params, "grant_type");
+        if (!GRANT_TYPE.getValue().equals(grantType)) {
+            throw new ParseException("The grant_type must be " + GRANT_TYPE.getValue(),
+                    OAuth2Error.UNSUPPORTED_GRANT_TYPE);
+        }
+        final String code = MultivaluedMapUtils.getFirstValue(params, PRE_AUTHORIZED_CODE_PARAM);
+        if (StringUtils.isBlank(code)) {
+            throw new ParseException("Missing or empty " + PRE_AUTHORIZED_CODE_PARAM + " parameter",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+        return new PreAuthorizedCodeGrant(code, MultivaluedMapUtils.getFirstValue(params, TX_CODE_PARAM));
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/SignedCredentialIssuerMetadataSuccessResponse.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/SignedCredentialIssuerMetadataSuccessResponse.java
index cb36807..a893e0c 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/SignedCredentialIssuerMetadataSuccessResponse.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/impl/SignedCredentialIssuerMetadataSuccessResponse.java
@@ -44,6 +44,16 @@ public class SignedCredentialIssuerMetadataSuccessResponse implements SuccessRes
         content = metadata.serialize();
     }
 
+    /**
+     * Get the serialized metadata document.
+     *
+     * @return serialized metadata document
+     */
+    @Nonnull
+    public String getContent() {
+        return content;
+    }
+
     /** {@inheritDoc} */
     @Override
     public boolean indicatesSuccess() {
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCITokenConfiguration.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCITokenConfiguration.java
index e237e4b..4c5f834 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCITokenConfiguration.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCITokenConfiguration.java
@@ -16,14 +16,103 @@
 
 package org.geant.shibboleth.plugin.openidvci.profile.config.impl;
 
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+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.OpenIDVCITokenRequest;
 import org.geant.shibboleth.plugin.openidvci.profile.config.OpenIDVCIConfiguration;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.GrantType;
+
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenConfiguration;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.StringSupport;
 
 /** Profile configuration for the Token endpoint. */
-public class DefaultOpenIDVCITokenConfiguration extends AbstractOpenIDVCIConfiguration{
+public class DefaultOpenIDVCITokenConfiguration extends AbstractOpenIDVCIConfiguration
+        implements OAuth2TokenConfiguration {
+
+    /** Lookup function to supply the enabled grant types. */
+    @Nonnull
+    private Function<ProfileRequestContext, Set<String>> grantTypesLookupStrategy;
 
     /** Constructor. */
     public DefaultOpenIDVCITokenConfiguration() {
         super(OpenIDVCIConfiguration.PROFILE_ID_TOKEN);
+        grantTypesLookupStrategy = FunctionSupport.constant(CollectionSupport.setOf(
+                GrantType.AUTHORIZATION_CODE.getValue(), OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH));
+        setRefreshTokensEnabled(false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull
+    @NonnullElements
+    @NotLive
+    @Unmodifiable
+    public Set<String> getGrantTypes(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Collection<String> types = grantTypesLookupStrategy.apply(profileRequestContext);
+
+        return types != null ? CollectionSupport.copyToSet(types) : CollectionSupport.emptySet();
+    }
+
+    /**
+     * Set the enabled grant types.
+     *
+     * @param types enabled grant types
+     */
+    public void setGrantTypes(@Nonnull @NonnullElements final Collection<String> types) {
+        Constraint.isNotNull(types, "Grant types cannot be null");
+
+        grantTypesLookupStrategy =
+                FunctionSupport.constant(Set.copyOf(StringSupport.normalizeStringCollection(types)));
+    }
+
+    /**
+     * Set the lookup strategy for the enabled grant types.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setGrantTypesLookupStrategy(@Nonnull final Function<ProfileRequestContext, Set<String>> strategy) {
+        grantTypesLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEnforceRefreshTokenRotation(@Nullable final ProfileRequestContext profileRequestContext) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public BiFunction<ProfileRequestContext, Map<String, Object>, Map<String, Object>>
+            getRefreshTokenClaimsSetManipulationStrategy(@Nullable final ProfileRequestContext profileRequestContext) {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isIssueIdTokenViaRefreshToken(@Nullable final ProfileRequestContext profileRequestContext) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isLimitInitialAccessTokenToSelf(@Nullable final ProfileRequestContext profileRequestContext) {
+        return false;
     }
 
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/BuildCredentialOfferToken.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/BuildCredentialOfferToken.java
index 8f946b4..098acab 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/BuildCredentialOfferToken.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/BuildCredentialOfferToken.java
@@ -57,8 +57,7 @@ import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrat
  * Action that builds credential offer token. Token value is either a key to
  * storage or a serialized {@link CredentialOfferClaimsSet}. Token is stored to
  * {@link CredentialOfferContext} to be returned in the response as the
- * pre-authorized_code. The authorization code flow carries no token of its own,
- * its offer holding only credential configuration ids.
+ * pre-authorized_code.
  */
 public class BuildCredentialOfferToken extends AbstractProfileAction {
 
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
index 7326be1..9d2edea 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
@@ -44,6 +44,7 @@ import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 
 import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.DPoPProofNonceJWTValidationException;
 import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
 import net.shibboleth.oidc.jwt.claims.JWTValidationException;
 import net.shibboleth.profile.context.RelyingPartyContext;
@@ -171,6 +172,10 @@ public class ParseProof extends AbstractProfileAction {
                         final SignedJWT singleProof = SignedJWT.parse(strToken);
                         validateJWTProof(singleProof, profileRequestContext);
                         proofs.add(singleProof);
+                    } catch (final DPoPProofNonceJWTValidationException e) {
+                        log.error("{} proof {} carries an invalid nonce.", getLogPrefix(), proof, e);
+                        ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_NONCE);
+                        return;
                     } catch (final Exception e) {
                         log.error("{} proof {} parsing failed.", getLogPrefix(), proof, e);
                         ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
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 5b89a6d..4267987 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.PreAuthorizedCodeGrant;
 import org.geant.shibboleth.plugin.openidvci.profile.context.navigate.APIRequestClientIDLookupFunction;
 import org.geant.shibboleth.plugin.openidvci.storage.CredentialOfferCache;
 import org.geant.shibboleth.plugin.openidvci.storage.CredentialOfferObject;
@@ -145,33 +146,31 @@ public class UnwrapGrant extends AbstractOpenIDVCITokenResponseAction {
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final AuthorizationGrant grant = getTokenRequest() != null ? getTokenRequest().getAuthorizationGrant() : null;
-        if (grant != null) {
-            log.debug("{} Unwrapping grant type: {}", getLogPrefix(), grant.getType());
+        final AuthorizationGrant grant = getTokenRequest().getAuthorizationGrant();
+        log.debug("{} Unwrapping grant type: {}", getLogPrefix(), grant.getType());
+        if (GrantType.AUTHORIZATION_CODE.equals(grant.getType())) {
             TokenClaimsSet tokenClaimsSet = null;
-            if (GrantType.AUTHORIZATION_CODE.equals(grant.getType())) {
-                final AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
-                if (codeGrant.getAuthorizationCode() != null && codeGrant.getAuthorizationCode().getValue() != null) {
-                    try {
-                        final String codeValue = codeGrant.getAuthorizationCode().getValue();
-                        assert codeValue != null;
-                        final AuthorizeCodeClaimsSet authzCodeClaimsSet = AuthorizeCodeClaimsSet.parse(codeValue,
-                                dataSealer);
-                        assert authzCodeClaimsSet != null;
-                        final String jti = authzCodeClaimsSet.getID();
-                        if (jti == null) {
-                            log.warn("{} Invalid contents in the authz code grant: no JTI", getLogPrefix());
-                            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
-                            return;
-                        }
-                        log.debug("{} Authz code unwrapped {}", getLogPrefix(), authzCodeClaimsSet.serialize());
-                        tokenClaimsSet = authzCodeClaimsSet;
-
-                    } catch (final DataSealerException | ParseException e) {
-                        log.warn("{} Unwrapping authz code failed: {}", getLogPrefix(), e.getMessage());
+            final AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
+            if (codeGrant.getAuthorizationCode() != null && codeGrant.getAuthorizationCode().getValue() != null) {
+                try {
+                    final String codeValue = codeGrant.getAuthorizationCode().getValue();
+                    assert codeValue != null;
+                    final AuthorizeCodeClaimsSet authzCodeClaimsSet = AuthorizeCodeClaimsSet.parse(codeValue,
+                            dataSealer);
+                    assert authzCodeClaimsSet != null;
+                    final String jti = authzCodeClaimsSet.getID();
+                    if (jti == null) {
+                        log.warn("{} Invalid contents in the authz code grant: no JTI", getLogPrefix());
                         ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                         return;
                     }
+                    log.debug("{} Authz code unwrapped {}", getLogPrefix(), authzCodeClaimsSet.serialize());
+                    tokenClaimsSet = authzCodeClaimsSet;
+
+                } catch (final DataSealerException | ParseException e) {
+                    log.warn("{} Unwrapping authz code failed: {}", getLogPrefix(), e.getMessage());
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+                    return;
                 }
             }
             validateTokenClaimsSet(profileRequestContext, tokenClaimsSet, true);
@@ -182,8 +181,7 @@ public class UnwrapGrant extends AbstractOpenIDVCITokenResponseAction {
                     .ensureSubcontext(TokenContext.class);
             tokenContext.setPotentialCredentials(tokenClaimsSet.getUserinfoDeliveryClaims());
             return;
-        } else if (getOpenIDVCITokenRequest().getPreAuthorizedCode() instanceof String) {
-            // This is pre-authorized grant
+        } else if (PreAuthorizedCodeGrant.GRANT_TYPE.equals(grant.getType())) {
             final String code = getOpenIDVCITokenRequest().getPreAuthorizedCode();
             log.info("{} Unwrapping pre-authorization code: {}", getLogPrefix(), code);
             final Exception e;
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract-api/abstract-api-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract-api/abstract-api-flow.xml
index 5255c2b..27d71bd 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract-api/abstract-api-flow.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract-api/abstract-api-flow.xml
@@ -19,11 +19,23 @@
     <evaluate expression="SelectRelyingPartyConfiguration"/>
     <evaluate expression="SelectProfileConfiguration"/>
     <evaluate expression="PostLookupPopulateAuditContext"/>
+    <evaluate expression="PopulateInboundInterceptContext"/>
     <evaluate expression="'proceed'"/>
-    <transition on="proceed" to="AuthenticationSetup"/>
+    <transition on="proceed" to="CheckInboundInterceptContext"/>
   </action-state>
-  
+
+  <decision-state id="CheckInboundInterceptContext">
+    <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+        then="AuthenticationSetup" else="DoInboundInterceptSubflow"/>
+  </decision-state>
+
+  <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
+    <input name="calledAsSubflow" value="true"/>
+    <transition on="proceed" to="AuthenticationSetup"/>
+  </subflow-state>
+
   <action-state id="AuthenticationSetup">
+    <evaluate expression="CallInboundMessageHandler" />
     <evaluate expression="InitializeAuthenticationContext" />
     <evaluate expression="'proceed'" />
     <transition on="proceed" to="DoDPoPProofValidation" />
@@ -40,7 +52,7 @@
   <action-state id="BuildResponseMessage">
     <evaluate expression="FormOutboundMessage"/>
     <evaluate expression="'proceed'"/>
-    <transition on="proceed" to="CommitResponse"/>
+    <transition on="proceed" to="PopulateOutboundInterceptContext"/>
   </action-state>
 
   <action-state id="HandleError">
@@ -50,11 +62,12 @@
     </on-entry>
     <evaluate expression="BuildErrorResponseFromEvent"/>
     <evaluate expression="'proceed'"/>
-    <transition on="proceed" to="CommitResponse"/>
+    <transition on="proceed" to="PopulateOutboundInterceptContext"/>
   </action-state>
 
   <end-state id="CommitResponse">
     <on-entry>
+      <evaluate expression="CallOutboundMessageHandler"/>
       <evaluate expression="EncodeMessage"/>
       <evaluate expression="PostResponsePopulateAuditContext"/>
       <evaluate expression="WriteAuditLog"/>
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract/abstract-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract/abstract-flow.xml
index 6f37da9..05daa35 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract/abstract-flow.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/abstract/abstract-flow.xml
@@ -4,6 +4,27 @@
       xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd" 
       abstract="true">
 
+  <action-state id="PopulateOutboundInterceptContext">
+    <evaluate expression="PopulateOutboundInterceptContext"/>
+    <evaluate expression="'proceed'"/>
+    <transition on="proceed" to="CheckOutboundInterceptContext"/>
+  </action-state>
+
+  <decision-state id="CheckOutboundInterceptContext">
+    <on-entry>
+      <evaluate expression="flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') != null ? flowRequestContext.getFlowScope().get('transitionAfterOutboundIntercept') : 'CommitResponse'" result="flowScope.postOutboundInterceptTransition"/>
+      <evaluate expression="PopulateOutboundInterceptContext"/>
+    </on-entry>
+    <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+        then="#{postOutboundInterceptTransition}" else="DoOutboundInterceptSubflow"/>
+  </decision-state>
+
+  <subflow-state id="DoOutboundInterceptSubflow" subflow="intercept">
+    <input name="calledAsSubflow" value="true"/>
+    <transition on="proceed" to="#{postOutboundInterceptTransition}"/>
+    <transition to="HandleError"/>
+  </subflow-state>
+
   <action-state id="LogRuntimeException">
     <on-entry>
       <evaluate expression="T(org.slf4j.LoggerFactory).getLogger('org.geant.shibboleth.plugin.openidvci.profile').error('Uncaught runtime exception', flowExecutionException.getCause())"/>
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 56fc501..ddeb85a 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
@@ -167,10 +167,43 @@
   <bean id="FormOutboundMessage" class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormOutboundCredentialsResponseMessage"
         scope="prototype"  />
 
+  <!-- Error vocabulary of the Credential Endpoint, OpenID4VCI 1.0 section 8.4. -->
+  <bean id="openidvci.credentials.MappedErrors" parent="shibboleth.oidc.DefaultOAuth2ProtectedApiMappedErrors"
+        class="org.springframework.beans.factory.config.MapFactoryBean">
+    <property name="sourceMap">
+      <map merge="true" value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+        <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).NO_CREDENTIALS_REQUEST}"
+            value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).INVALID_CREDENTIAL_REQUEST}" />
+        <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).NO_CREDENTIAL_REQUEST}"
+            value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).INVALID_CREDENTIAL_REQUEST}" />
+        <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).NO_CREDENTIAL_CONFIGURATION}"
+            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).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}"
+            value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).INVALID_PROOF}" />
+        <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).INVALID_NONCE}"
+            value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).INVALID_NONCE}" />
+        <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).INVALID_CREDENTIAL}"
+            value="#{T(org.geant.shibboleth.plugin.openidvci.messaging.error.OpenIDVCIError).CREDENTIAL_REQUEST_DENIED}" />
+        <entry key="#{T(org.opensaml.profile.action.EventIds).INVALID_PROFILE_CTX}"
+            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+        <entry key="#{T(org.opensaml.profile.action.EventIds).INVALID_MSG_CTX}"
+            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+        <entry key="#{T(org.opensaml.profile.action.EventIds).INVALID_SEC_CFG}"
+            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+        <entry key="#{T(org.opensaml.profile.action.EventIds).IO_ERROR}"
+            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+      </map>
+    </property>
+  </bean>
+
   <bean id="BuildErrorResponseFromEvent"
         class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.BuildUserInfoErrorResponseFromEvent" scope="prototype"
         p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
-        p:mappedErrors="#{getObject('shibboleth.oidc.userinfo.MappedErrors') ?: getObject('shibboleth.oidc.DefaultOAuth2ProtectedApiMappedErrors')}"
+        p:mappedErrors="#{getObject('shibboleth.oidc.userinfo.MappedErrors') ?: getObject('openidvci.credentials.MappedErrors')}"
         p:securityParametersContextLookupStrategy-ref="DPoPSecurityParametersContextProfileRequestContextLookup"
         p:algorithmCandidates="%{idp.oauth2.dpop.proofAlgorithms:RS256,RS384,RS512,PS256,PS384,PS512,ES256,ES384,ES512}"
         >
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 4fad799..f7958ce 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
@@ -1,33 +1,88 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<beans
- xmlns="http://www.springframework.org/schema/beans"
- xmlns:c="http://www.springframework.org/schema/c"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:p="http://www.springframework.org/schema/p"
- xmlns:util="http://www.springframework.org/schema/util"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
-       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
-                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
-                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd" 
-       default-init-method="initialize" default-destroy-method="destroy">
-       
-  <bean id="openidvci.profileId" class="java.lang.String" c:_0="http://geant.org/ns/profiles/openid/vci/token"/>
-  <bean id="openidvci.loggingId" class="java.lang.String"
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <!-- REPLACED shibboleth.oidc.profileId, legacyProfileId and loggingId. -->
+    <bean id="openidvci.profileId" class="java.lang.String" c:_0="http://geant.org/ns/profiles/openid/vci/token"/>
+
+    <bean id="openidvci.loggingId" class="java.lang.String"
         c:_0="%{openidvci.logging.token:OpenID.VCI.Token}"/>
-  
-  <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
-    <constructor-arg>
-      <bean class="org.geant.shibboleth.plugin.openidvci.decoding.impl.OpenIDVCITokenRequestDecoder" 
-            scope="prototype" p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
-    </constructor-arg>
-  </bean>
-  
-  <bean id="UnwrapOrLocateGrant" class="org.geant.shibboleth.plugin.openidvci.profile.impl.UnwrapGrant" scope="prototype"
+
+    <bean id="shibboleth.oidc.MetadataEnforcedDPoP" parent="shibboleth.Conditions.Expression"
+        c:expression="#input.ensureInboundMessageContext().getSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)) != null and #input.ensureInboundMessageContext().getSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation() != null and #input.ensureInboundMessageContext().getSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation().getMetadata().toJSONObject().get('dpop_bound_access_tokens') !=  [...]
+
+    <util:constant id="shibboleth.metrics.ProfileCounter"
+        static-field="net.shibboleth.oidc.profile.oauth2.config.impl.DefaultOAuth2TokenConfiguration.PROFILE_COUNTER" />
+
+    <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+        <constructor-arg>
+            <!-- REPLACED OIDCTokenRequestDecoder. -->
+            <bean class="org.geant.shibboleth.plugin.openidvci.decoding.impl.OpenIDVCITokenRequestDecoder"
+                scope="prototype" p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+        </constructor-arg>
+    </bean>
+
+    <!-- REPLACED TokenRequestClientIDLookupFunction. -->
+    <bean id="shibboleth.ClientIDLookupStrategy" class="org.geant.shibboleth.plugin.openidvci.profile.context.navigate.APIRequestClientIDLookupFunction"
+        p:credentialOfferCache-ref="openidvci.CredentialOfferCache"
+        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}" />
+
+    <bean id="openidvci.ProfileRequestClientIDLookupStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ClientIDLookupStrategy"
+        c:f-ref="shibboleth.MessageContextLookup.Inbound" />
+
+    <!-- REPLACED UnwrapGrant. -->
+    <bean id="OpenIDVCIUnwrapGrant" class="org.geant.shibboleth.plugin.openidvci.profile.impl.UnwrapGrant" scope="prototype"
         c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
         p:clientIDLookupStrategy-ref="openidvci.ProfileRequestClientIDLookupStrategy"
         p:credentialOfferCache-ref="openidvci.CredentialOfferCache"/>
-  
-  <bean id="ValidateGrant" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidateGrant" scope="prototype"
+
+    <bean id="ResolveAttributesForClientPredicate"
+        class="net.shibboleth.profile.config.logic.ResolveAttributesPredicate" />
+
+    <bean id="ResolveAttributesForAudiencePredicate"
+        class="net.shibboleth.profile.config.logic.ResolveAttributesPredicate"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundResponseMessageContext"
+        scope="prototype" />
+
+    <bean id="ValidateClientIDAgainstPolicy"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateClientIDAgainstPolicy"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+        scope="prototype"
+        p:unregisteredClientPolicyEnforcer="#{getObject('shibboleth.oidc.UnregisteredClientPolicyEnforcer') ?: getObject('shibboleth.oidc.DefaultUnregisteredClientPolicyEnforcer')}"/>
+
+    <bean id="ValidateGrantType" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrantType"
+        scope="prototype" />
+
+    <!-- Condition signaling that request was NOT for client_credentials grant. -->        
+    <bean id="NotClientCredentialsGrantCondition" parent="shibboleth.Conditions.NOT">
+        <constructor-arg>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
+                p:grantTypes="#{T(com.nimbusds.oauth2.sdk.GrantType).CLIENT_CREDENTIALS}" />
+        </constructor-arg>
+    </bean>
+
+    <!-- Condition signaling that request was for authorizaton_code grant. -->        
+    <bean id="AuthorizationCodeGrantCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
+        p:grantTypes="#{T(com.nimbusds.oauth2.sdk.GrantType).AUTHORIZATION_CODE}" />
+
+    <!-- Condition signaling that request was for refresh_token grant. -->        
+    <bean id="RefreshTokenGrantCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
+        p:grantTypes="#{T(com.nimbusds.oauth2.sdk.GrantType).REFRESH_TOKEN}" />
+
+    <!-- Traditional third-party grant handling. -->
+
+    <bean id="ValidateGrant" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrant" scope="prototype"
         p:replayCache-ref="shibboleth.ReplayCache"
         p:revocationCache-ref="shibboleth.oidc.RevocationCache"
         p:tokenRevocationCondition="#{getObject('%{idp.oauth2.revocationCondition:shibboleth.BiConditions.FALSE}')}">
@@ -35,101 +90,791 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy"
                 p:clockSkew="%{idp.policy.clockSkew:PT5M}" p:useActiveProfileOnly="false" />
         </property>
-  </bean>
-
-  <bean id="ValidateExpectedGrantType" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidateExpectedGrantType"
-        scope="prototype"/>
+    </bean>
 
-  <bean id="ValidatePKCE" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidatePKCE" scope="prototype" 
+    <!-- REPLACED WIRING. Code verifier lookup of this plugin. -->
+    <bean id="ValidatePKCE" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidatePKCE" scope="prototype"
         p:codeVerifierLookupStrategy-ref="RequestCodeVerifierLookupFunction"/>
-       
-  <bean id="RequestCodeVerifierLookupFunction" class="org.geant.shibboleth.plugin.openidvci.messaging.context.navigate.RequestCodeVerifierLookupFunction" />
 
-  <bean id="ValidateTxCode" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidateTxCode"
-        scope="prototype"/>
+    <bean id="RequestCodeVerifierLookupFunction" class="org.geant.shibboleth.plugin.openidvci.messaging.context.navigate.RequestCodeVerifierLookupFunction" />
 
-  <bean id="ValidatePreAuthorizedCode" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidatePreAuthorizedCode"
-        scope="prototype" p:replayCache-ref="shibboleth.ReplayCache"/>
-        
-  <bean id="SetAuthorizationDetailsToResponseContext" class="org.geant.shibboleth.plugin.openidvci.profile.impl.SetAuthorizationDetailsToResponseContext"
-        scope="prototype"/>
+    <bean id="ValidateRedirectURI" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateRedirectURI"
+        scope="prototype"
+        p:activationCondition-ref="AuthorizationCodeGrantCondition"
+        p:redirectURILookupStrategy-ref="shibboleth.TokenRequestRedirectURILookupStrategy"
+        p:validRedirectURIsLookupStrategy-ref="shibboleth.TokenRequestValidRequestUrisLookupStrategy"
+        p:requireRequestedValue="false"
+        p:unregisteredClientPolicyEnforcer="#{getObject('shibboleth.oidc.UnregisteredClientPolicyEnforcer') ?: getObject('shibboleth.oidc.DefaultUnregisteredClientPolicyEnforcer')}"/>
 
-  <bean id="InitializeAuthenticationContext" class="org.geant.shibboleth.plugin.openidvci.profile.impl.InitializeAuthenticationContext"
-        scope="prototype" />
-        
-  <bean id="SetRequestedClaimsToResponseContext"
+    <bean id="shibboleth.TokenRequestRedirectURILookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.TokenRequestRedirectURILookupFunction" />
+
+    <bean id="shibboleth.TokenRequestValidRequestUrisLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestValidRequestURIsLookupFunction" />
+
+    <bean id="SetRequestedClaimsToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRequestedClaimsToResponseContext" scope="prototype"
         p:requestedClaimsLookupStrategy-ref="shibboleth.TokenRequestRequestedClaimsLookupFunction"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
 
-  <bean id="shibboleth.TokenRequestRequestedClaimsLookupFunction"
+    <bean id="shibboleth.TokenRequestRequestedClaimsLookupFunction"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestRequestedClaimsLookupFunction" />
 
-  <bean id="SetAuthenticationContextClassReferenceFromAuthzCodeToResponseContext"
+    <bean id="SetAuthenticationContextClassReferenceFromAuthzCodeToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceFromAuthzCodeToResponseContext"
         scope="prototype" />
 
-  <bean id="SetAuthenticationTimeFromAuthzCodeToResponseContext"
+    <bean id="SetAuthenticationTimeFromAuthzCodeToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype"
         p:authTimeLookupStrategy-ref="shibboleth.TokenRequestAuthTimeLookupFunction" />
 
-  <bean id="shibboleth.TokenRequestAuthTimeLookupFunction"
+    <bean id="shibboleth.TokenRequestAuthTimeLookupFunction"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestAuthTimeLookupFunction" />
 
-  <bean id="SetTokenDeliveryAttributesFromTokenToResponseContext"
+    <bean id="SetTokenDeliveryAttributesFromTokenToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetTokenDeliveryAttributesFromTokenToResponseContext"
         scope="prototype" />
 
-  <bean id="SetConsentToResponseContext"
+    <bean id="SetConsentToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetConsentFromTokenToResponseContext" scope="prototype" />
 
-  <bean id="InitializeSubjectContext"
+    <bean id="InitializeSubjectContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeSubjectContext" scope="prototype" />
 
-  <bean id="SetSubjectFromAuthzCodeToResponseContext"
+    <bean id="SetSubjectFromAuthzCodeToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype" />
-  
-  <bean id="BuildAccessToken"
+
+    <bean id="SetSectorIdentifierForAttributeResolution"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSectorIdentifierForAttributeResolution" scope="prototype" />
+
+    <!-- client_credentials grant handling. -->
+
+    <bean id="SetAuthenticationContextClassReferenceToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceToResponseContext"
+        scope="prototype" />
+
+    <bean id="SetAuthenticationTimeToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype" />
+
+    <bean id="SetSubjectFromSubjectContextToResponseContext"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype">
+        <property name="subjectLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg>
+                    <bean class="net.shibboleth.idp.authn.context.navigate.SubjectContextPrincipalLookupFunction" />
+                </constructor-arg>
+                <constructor-arg>
+                    <ref bean="shibboleth.ChildLookup.SubjectContext"/>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="SetSessionIdToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSessionIdToResponseContext" scope="prototype"
+        p:sessionIdLookupStrategy-ref="SessionIdGenerationStrategy" />
+
+    <bean id="SetSessionIdFromAuthzCodeToResponseContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSessionIdToResponseContext" scope="prototype" />
+
+    <!-- Common grant handling.  -->
+
+    <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateScope" scope="prototype"
+        p:requestedScopeLookupStrategy-ref="TokenRequestScopeLookupStrategy"
+        p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
+
+    <bean id="ValidateAudience"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateAudience" scope="prototype"
+        p:allowedAudienceLookupStrategy="#{getObject('shibboleth.oidc.AllowedAudienceStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedAudienceStrategy')}">
+        <property name="selfAudienceCondition">
+            <bean parent="shibboleth.Conditions.AND">
+                <constructor-arg>
+                    <list>
+                        <ref bean="BuildOIDCTokensCondition" />
+                        <ref bean="NotClientCredentialsGrantCondition" />
+                    </list>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="TokenRequestScopeLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.TokenRequestScopeLookupFunction" />
+
+    <bean id="BuildOIDCTokensCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.IssueIDTokenCondition" />
+
+    <bean id="IssueIDTokenCondition" parent="shibboleth.Conditions.AND">
+        <constructor-arg>
+            <list>
+                <ref bean="BuildOIDCTokensCondition" />
+                <bean parent="shibboleth.Conditions.OR">
+                    <constructor-arg>
+                        <list>
+                            <ref bean="AuthorizationCodeGrantCondition" />
+                            <bean parent="shibboleth.Conditions.AND">
+                                <constructor-arg>
+                                    <list>
+                                        <ref bean="RefreshTokenGrantCondition" />
+                                        <bean class="net.shibboleth.oidc.profile.config.logic.IssueIdTokenViaRefreshTokenPredicate" />
+                                    </list>
+                                </constructor-arg>
+                            </bean>
+                        </list>
+                    </constructor-arg>
+                </bean>
+            </list>
+        </constructor-arg>
+    </bean>
+
+    <!--
+    Do a metadata lookup for the primary audience of the token for encryption purposes.
+    Contexts are stored under the outbound MessageContext, including the new RelyingPartyContext.
+    -->
+    
+    <bean id="AudienceOIDCMetadataLookup" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.impl.OIDCMetadataLookupHandler" scope="prototype">
+                <property name="clientInformationResolver">
+                    <ref bean="shibboleth.ClientInformationResolver" />
+                </property>
+                <property name="clientIDLookupStrategy">
+                    <ref bean="AudienceClientIDLookupStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+    
+    <bean id="AudienceClientIDLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.AudienceClientIDLookupFunction" />
+
+    <bean id="InitializeAudienceRelyingPartyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype"
+        p:relyingPartyContextCreationStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:oidcMetadataContextLookupStrategy-ref="LookupOutboundOIDCMetadataContext"
+        p:clientIDLookupStrategy-ref="AudienceClientIDLookupStrategy"
+        p:inbound="false" />
+
+    <bean id="LookupOutboundOIDCMetadataContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction"
+        p:inbound="false" />
+
+    <bean id="AudienceRelyingPartyCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.RelyingPartyContext"
+        c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+
+    <bean id="AudienceSAMLProtocolAndRole"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="OUTBOUND"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.saml.common.binding.impl.SAMLProtocolAndRoleHandler" scope="prototype"
+                p:protocol="http://openid.net/specs/openid-connect-core-1_0.html"
+                p:role-ref="shibboleth.MetadataLookup.Role"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="SetAudienceEntityIdToSAMLPeerEntityContext"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="OUTBOUND"
+            p:errorEvent="#{T(org.opensaml.profile.action.EventIds).INVALID_MSG_CTX}"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.SetEntityIdToSAMLPeerEntityContext"
+                scope="prototype"
+                p:clientIDLookupStrategy-ref="AudienceClientIDLookupStrategy"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext" />
+        </constructor-arg>
+    </bean>
+ 
+     <bean id="AudienceSAMLMetadataLookup"
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+        c:executionDirection="OUTBOUND"
+        p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler" scope="prototype"
+                p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext">
+                <property name="roleDescriptorResolver">
+                    <bean class="org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver"
+                        c:mdResolver-ref="shibboleth.MetadataResolver" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+ 
+     <bean id="PopulateAudienceOIDCMetadataContext"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="OUTBOUND"
+            p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+        <constructor-arg name="messageHandler">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.PopulateOIDCMetadataContext"
+                scope="prototype" />
+        </constructor-arg>
+    </bean>
+        
+    <bean id="InitializeAudienceRelyingPartyContextFromSAMLPeer"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype"
+        p:relyingPartyContextCreationStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:peerEntityContextLookupStrategy-ref="LookupOutboundPeerEntityContext" />
+
+    <bean id="LookupOutboundPeerEntityContext" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookup.SAMLPeerEntityContext"
+        c:f-ref="shibboleth.MessageContextLookup.Outbound"/>
+
+    <bean id="SelectAudienceRelyingPartyConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyResolverService" />
+
+    <bean id="SelectAudienceProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+        p:profileId="#{T(net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenAudienceConfiguration).PROFILE_ID}" />
+
+    <bean id="ResolveAttributesForAudience" class="net.shibboleth.idp.profile.impl.ResolveAttributes" scope="prototype"
+        c:resolverService-ref="shibboleth.AttributeResolverService"
+        p:maskFailures="%{idp.service.attribute.resolver.maskFailures:true}"
+        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction" />
+
+    <bean id="FilterAttributesForAudience" class="net.shibboleth.idp.profile.impl.FilterAttributes" scope="prototype"
+        c:filterService-ref="shibboleth.AttributeFilterService"
+        p:maskFailures="%{idp.service.attribute.filter.maskFailures:true}"
+        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+        p:proxiedRequesterContextLookupStrategy-ref="AudienceProxiedRequesterLookupFunction"
+        p:proxiedRequesterMetadataContextLookupStrategy-ref="LookupOutboundSAMLEntityContext" />
+
+    <bean id="AudienceIssuerLookupFunction"
+        class="net.shibboleth.profile.context.navigate.IssuerLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <bean id="AudienceProxiedRequesterLookupFunction" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+                c:type="#{ T(org.opensaml.profile.context.ProxiedRequesterContext) }" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.MessageContextLookup.Outbound" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="LookupOutboundSAMLEntityContext" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookup.SAMLMetadataContext"
+        c:f-ref="LookupOutboundPeerEntityContext"/>
+
+    <!-- Back to token prep. -->
+    
+    <!-- OIDC response handling for access/refresh tokens. -->
+
+    <bean id="PopulateUserInfoAccessTokenSignatureSigningParameters"
+            class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
+            scope="prototype"
+            c:strategy-ref="shibboleth.MessageContextLookup.Inbound">
+        <property name="configurationLookupStrategy">
+            <bean lazy-init="true"
+                class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+        </property>
+        <property name="signatureSigningParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+                <constructor-arg name="signatureAlgorithmLookupStrategy">
+                    <bean
+                        class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                        c:keyName="id_token_signed_response_alg" />
+                </constructor-arg>
+                <constructor-arg name="defaultAlgorithmValue" value="RS256" />
+            </bean>
+        </property>
+        <property name="securityParametersContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
+    </bean>
+
+    <!-- REPLACED WIRING. Client id lookup of this plugin. -->
+    <bean id="BuildOIDCAccessToken"
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
         p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
         p:xmlSafeIdentifier="%{idp.oidc.xmlSafeIdentifiers:true}"
         p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
         p:clientIDLookupStrategy-ref="openidvci.ProfileRequestClientIDLookupStrategy"
         p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
-              
-  <bean id="SetOAuthAccessTokenToResponseContext"
+
+    <bean id="SignOIDCAccessToken" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean id="SignOIDCAccessTokenHandler"
+                class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Access Token"
+                p:typeHeader="at+jwt">
+                <property name="securityParametersLookupStrategy">
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="g">
+                            <bean parent="shibboleth.Functions.Compose"
+                                c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+                                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+                        </constructor-arg>
+                        <constructor-arg name="f">
+                            <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="claimsToSignLookupStrategy">
+                     <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.JWTClaimsSetFromJWTAccessTokenLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.JWTAccessTokenUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+ 
+    <bean id="SetOAuthAccessTokenToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
         scope="prototype" />
 
-  <bean id="ConsumeCredentialOffer" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ConsumeCredentialOffer"
-        scope="prototype" p:credentialOfferCache-ref="openidvci.CredentialOfferCache" />
+    <bean id="SetRefreshTokenToResponseContext"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRefreshTokenToResponseContext" scope="prototype"
+            c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+            p:revocationCache-ref="shibboleth.oidc.RevocationCache"
+            p:activationCondition-ref="#{'%{idp.oauth2.refreshToken.activation:DefaultRefreshTokenActivationCondition}'.trim()}"
+            p:refreshTokenSerializationStrategies-ref="#{'%{idp.oauth2.refreshToken.serializationStrategies:shibboleth.oidc.DefaultRefreshTokenSerializationStrategies}'.trim()}"
+            p:xmlSafeIdentifier="%{idp.oidc.xmlSafeIdentifiers:true}"
+            p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy">
+        <property name="tokenRevocationLifetimeLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultTokenRevocationLifetimeLookupStrategy"
+                p:clockSkew="%{idp.policy.clockSkew:PT5M}" />
+        </property>
+    </bean>
+
+    <util:map id="shibboleth.oidc.DefaultRefreshTokenSerializationStrategies">
+        <entry key="JWT">
+            <bean factory-bean="DefaultJwtRefreshTokenSerializerFactory" factory-method="getBean" />
+        </entry>
+    </util:map>
+
+    <bean id="DefaultJwtRefreshTokenSerializerFactory"
+        parent="shibboleth.oidc.RefreshTokenSerializerFactory"
+        c:id="DefaultJwtRefreshTokenSerializationFunction"/>
+
+    <bean id="DefaultJwtRefreshTokenSerializationFunction"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultJwtRefreshTokenSerializationFunction"
+        scope="prototype"
+        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}">
+        <property name="signingParametersHandler">
+            <bean class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParametersHandler"
+                scope="prototype">
+                <property name="configurationLookupStrategy">
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="f">
+                            <bean class="org.opensaml.messaging.context.navigate.ParentContextLookup"
+                                c:type="org.opensaml.profile.context.ProfileRequestContext" />
+                        </constructor-arg>
+                        <constructor-arg name="g">
+                            <bean lazy-init="true"
+                                class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="signatureSigningParametersResolver">
+                    <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+                        <constructor-arg name="signatureAlgorithmLookupStrategy">
+                            <bean class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                                c:keyName="id_token_signed_response_alg" />
+                        </constructor-arg>
+                        <constructor-arg name="defaultAlgorithmValue" value="RS256" />
+                    </bean>
+                </property>
+            </bean>
+        </property>
+        <property name="audienceLookupStrategy">
+            <bean parent="shibboleth.Functions.Expression"
+                c:expression="#custom.get().getRequestURL().toString()"
+                p:customObject-ref="shibboleth.HttpServletRequestSupplier" />
+        </property>
+        <property name="typeHeaderLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression" c:expression="#null" />
+        </property>
+    </bean>
+
+
+    <bean id="DefaultRefreshTokenActivationCondition" parent="shibboleth.Conditions.AND">
+        <constructor-arg>
+            <list>
+                <bean class="net.shibboleth.oidc.profile.config.logic.RefreshTokensEnabledPredicate" />
+                <ref bean="NotClientCredentialsGrantCondition" />
+                <bean parent="shibboleth.Conditions.OR">
+                    <constructor-arg>
+                        <list>
+                            <bean parent="shibboleth.Conditions.NOT">
+                                <constructor-arg>
+                                    <ref bean="BuildOIDCTokensCondition" />
+                                </constructor-arg>
+                            </bean>
+                            <bean parent="shibboleth.Conditions.AND">
+                                <constructor-arg>
+                                    <list>
+                                        <ref bean="BuildOIDCTokensCondition" />
+                                        <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.OfflineAccessScopeCondition" />
+                                    </list>
+                                </constructor-arg>
+                            </bean>
+                        </list>
+                    </constructor-arg>
+                </bean>
+            </list>
+        </constructor-arg>
+    </bean>
+
+    <!-- ID token actions. -->
+
+    <bean id="PopulateIDTokenSignatureSigningParameters"
+            class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
+            scope="prototype" p:noResultIsError="true"
+            c:strategy-ref="shibboleth.MessageContextLookup.Inbound">
+        <property name="configurationLookupStrategy">
+            <bean lazy-init="true"
+                class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+        </property>
+        <property name="signatureSigningParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+                <constructor-arg name="signatureAlgorithmLookupStrategy">
+                    <bean
+                        class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                        c:keyName="id_token_signed_response_alg" />
+                </constructor-arg>
+                <constructor-arg name="defaultAlgorithmValue" value="RS256" />
+            </bean>
+        </property>
+         <property name="securityParametersContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+        </property>
+    </bean>
+
+    <bean id="PopulateIDTokenEncryptionParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTEncryptionParameters" scope="prototype"
+        p:forFriendlyName="ID Token">
+        <property name="clientMetadataContextLookupStrategy">
+           <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction"
+               p:inbound="true" />
+        </property>
+        <property name="configurationLookupStrategy">
+            <bean lazy-init="true"
+                class="net.shibboleth.oidc.profile.config.navigate.JWTEncryptionConfigurationLookupFunction" />
+        </property>
+        <property name="encryptionParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.DefaultEncryptionParametersResolver">
+                <property name="keyTransportEncryptionAlgorithmsLookupStrategy">
+                    <bean
+                        class="net.shibboleth.oidc.security.jose.impl.ClientInformationKeyTransportEncryptionAlgorithmsLookupStrategy">
+                        <constructor-arg>
+                            <bean
+                                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                                c:keyName="id_token_encrypted_response_alg" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="dataEncryptionAlgorithmsLookupStrategy">
+                    <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationDataEncryptionAlgorithmsLookupStrategy">
+                        <constructor-arg>
+                            <bean
+                                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                                c:keyName="id_token_encrypted_response_enc" />
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
 
-  <bean id="FormOutboundMessage" class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormOutboundTokenResponseMessage"
+    <bean id="AddIDTokenShell"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell" scope="prototype" />
+
+    <bean id="AddAttributeClaimsToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
+        p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
+        p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}" />
+
+    <bean id="AddTokenDeliveryAttributesToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddTokenDeliveryAttributesToClaimsSet" scope="prototype"
+        p:targetIDToken="true" />
+
+    <bean id="AddAuthTimeToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthTimeToIDToken" scope="prototype" />
+
+    <bean id="AddAcrToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken" scope="prototype" />
+
+    <bean id="AddNonceToIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddNonceToIDToken" scope="prototype"
+        p:requestNonceLookupStrategy-ref="shibboleth.TokenRequestNonceLookupStrategy" />
+
+    <bean id="shibboleth.TokenRequestNonceLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestNonceLookupFunction" />
+
+    <bean id="AddAccessTokenHashToIDToken"
+            class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype">
+        <property name="securityParametersLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+        </property>
+    </bean>
+
+    <bean id="ManipulateClaimsForIDToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ManipulateClaimsForIDToken" scope="prototype"
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
+
+    <bean id="SignIDToken" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean id="SignIDTokenHandler"
+                class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype"  p:logName="ID Token">
+                <property name="claimsToSignLookupStrategy">
+                     <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.JWTClaimsSetFromIDTokenLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ProcessedTokenUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="EncryptIDToken" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean id="EncryptIDTokenHandler"
+                class="net.shibboleth.oidc.security.impl.EncryptJWTHandler" scope="prototype"
+                p:logName="ID Token">
+                <property name="payloadToEncryptLookupStrategy">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.PayloadFromProcessedTokenLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ProcessedTokenUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <!-- REPLACED FormOutboundTokenResponseMessage. -->
+    <bean id="FormOutboundMessage" class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormOutboundTokenResponseMessage"
         scope="prototype" />
 
-  <bean id="BuildErrorResponseFromEvent"
+    <bean id="BuildErrorResponseFromEvent"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.BuildTokenErrorResponseFromEvent" scope="prototype"
         p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
-        p:mappedErrors="#{getObject('shibboleth.oidc.token.MappedErrors') ?: getObject('shibboleth.oidc.DefaultApiMappedErrors')}">
+        p:mappedErrors="#{getObject('shibboleth.oidc.token.MappedErrors') ?: getObject('openidvci.token.MappedErrors')}">
         <property name="eventContextLookupStrategy">
             <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
         </property>
     </bean>
 
-  <bean id="shibboleth.ClientIDLookupStrategy" class="org.geant.shibboleth.plugin.openidvci.profile.context.navigate.APIRequestClientIDLookupFunction"
-        p:credentialOfferCache-ref="openidvci.CredentialOfferCache"
-        p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}" />
-        
-  <bean id="openidvci.ProfileRequestClientIDLookupStrategy" parent="shibboleth.Functions.Compose"
-        c:g-ref="shibboleth.ClientIDLookupStrategy"
-        c:f-ref="shibboleth.MessageContextLookup.Inbound" />
+    <!-- Third-party token actions. -->
+
+    <bean id="PopulateThirdPartyAccessTokenSignatureSigningParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
+        scope="prototype" p:noResultIsError="true"
+        p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy"
+        c:strategy-ref="shibboleth.MessageContextLookup.Outbound">
+        <property name="configurationLookupStrategy">
+            <bean lazy-init="true"
+                class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+        </property>
+        <property name="signatureSigningParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+                <constructor-arg name="signatureAlgorithmLookupStrategy">
+                    <bean
+                        class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                        c:keyName="id_token_signed_response_alg" />
+                </constructor-arg>
+                <constructor-arg name="defaultAlgorithmValue" value="RS256" />
+            </bean>
+        </property>
+    </bean>        
+
+    <bean id="AudienceSecurityParametersCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.oidc.ChildLookupOrCreate.JWTSecurityParameters"
+        c:f-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <bean id="AudienceSecurityParametersCreationViaMessageContextStrategy" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g" ref="AudienceSecurityParametersCreationStrategy" />
+        <constructor-arg name="f">
+            <bean parent="shibboleth.Functions.Expression" c:expression="#input.getParent()" />
+        </constructor-arg>
+    </bean>
         
-  <bean id="codeFlow" parent="shibboleth.Conditions.Expression" p:customObject-ref="shibboleth.HttpServletRequestSupplier">
-    <constructor-arg>
-      <value>
-        #custom.get().getParameter('code') != null
-      </value>
-    </constructor-arg>
-  </bean>
+    <bean id="PopulateThirdPartyAccessTokenEncryptionParameters"
+            class="net.shibboleth.oidc.profile.impl.PopulateJWTEncryptionParameters" scope="prototype"
+            p:encryptionOptionalPredicate-ref="AudienceEncryptionOptionalPredicate"
+            p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy"
+            p:forFriendlyName="JWT Access Token">
+        <property name="clientMetadataContextLookupStrategy">
+           <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction"
+               p:inbound="false" />
+        </property>
+        <property name="configurationLookupStrategy">
+            <bean lazy-init="true"
+                class="net.shibboleth.oidc.profile.config.navigate.JWTEncryptionConfigurationLookupFunction" />
+        </property>
+        <property name="encryptionParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.DefaultEncryptionParametersResolver">
+                <property name="keyTransportEncryptionAlgorithmsLookupStrategy">
+                    <bean
+                        class="net.shibboleth.oidc.security.jose.impl.ClientInformationKeyTransportEncryptionAlgorithmsLookupStrategy">
+                        <constructor-arg>
+                            <bean
+                                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                                c:keyName="id_token_encrypted_response_alg"/>
+                        </constructor-arg>
+                    </bean>
+                </property>
+                <property name="dataEncryptionAlgorithmsLookupStrategy">
+                    <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationDataEncryptionAlgorithmsLookupStrategy">
+                        <constructor-arg>
+                            <bean
+                                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                                c:keyName="id_token_encrypted_response_enc"/>
+                        </constructor-arg>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.NOT" c:_0-ref="BuildOIDCTokensCondition" />
+        </property>
+    </bean>
+
+    <bean id="AudienceEncryptionOptionalPredicate"
+        class="net.shibboleth.oidc.profile.config.logic.EncryptionOptionalPredicate"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />    
+
+    <bean id="AudienceEncryptionContextCreationStrategy" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookupOrCreate.EncryptionParameters"
+        c:f-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <bean id="AddAttributeClaimsToAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
+        p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
+        p:responseClaimsSetLookupStrategy-ref="AccessTokenClaimsSetLookupFunction"
+        p:reservedClaimNames="#{getObject('shibboleth.oidc.AccessTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultAccessTokenReservedClaimNames')}" />
+
+    <bean id="AccessTokenClaimsSetLookupFunction"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.AccessTokenClaimsSetLookupFunction"
+        p:autoCreate="true" />
+
+    <bean id="BuildAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
+        p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
+        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+        p:accessTokenTypeLookupStrategy-ref="AccessTokenTypeLookupFunction"
+        p:accessTokenLifetimeLookupStrategy-ref="AccessTokenLifetimeLookupFunction"
+        p:xmlSafeIdentifier="%{idp.oidc.xmlSafeIdentifiers:true}"
+        p:identifierGeneratorLookupStrategy-ref="shibboleth.oidc.DefaultIdentifierGenerationStrategy"
+        p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}" />
+
+    <bean id="AccessTokenTypeLookupFunction"
+        class="net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
         
+    <bean id="AccessTokenLifetimeLookupFunction"
+        class="net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction"
+        p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+    <bean id="SignAccessToken" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean id="SignAccessTokenHandler"
+                class="net.shibboleth.oidc.security.impl.SignJWTHandler" scope="prototype" p:logName="Access Token"
+                p:securityParametersLookupStrategy-ref="AudienceSecurityParametersCreationViaMessageContextStrategy"
+                p:typeHeader="at+jwt">
+                <property name="claimsToSignLookupStrategy">
+                     <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.JWTClaimsSetFromJWTAccessTokenLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.JWTAccessTokenUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="EncryptAccessToken" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+            scope="prototype" c:executionDirection="OUTBOUND">
+        <constructor-arg name="messageHandler">
+            <bean id="EncryptAccessTokenHandler"
+                class="net.shibboleth.oidc.security.impl.EncryptJWTHandler" scope="prototype"
+                p:securityParametersLookupStrategy-ref="AudienceSecurityParametersCreationViaMessageContextStrategy"
+                p:logName="Access Token">
+                <property name="payloadToEncryptLookupStrategy">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.PayloadFromJWTAccessTokenLookupFunction" />
+                </property>
+                <property name="jwtUpdateConsumer">
+                    <bean
+                        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.JWTAccessTokenUpdateStrategy" />
+                </property>
+            </bean>
+        </constructor-arg>
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.NOT" c:_0-ref="BuildOIDCTokensCondition" />
+        </property>
+    </bean>
+
+    <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidc.TokenPostResponseAuditExtractors') ?: getObject('shibboleth.oidc.DefaultTokenPostResponseAuditExtractors')}" />
+
+    <!-- ADDED. Actions and condition of the pre-authorized code grant. -->
+
+    <bean id="OpenIDVCIValidateTxCode" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidateTxCode"
+        scope="prototype"/>
+
+    <bean id="OpenIDVCIValidatePreAuthorizedCode" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidatePreAuthorizedCode"
+        scope="prototype" p:replayCache-ref="shibboleth.ReplayCache"/>
+
+    <bean id="OpenIDVCISetAuthorizationDetailsToResponseContext" class="org.geant.shibboleth.plugin.openidvci.profile.impl.SetAuthorizationDetailsToResponseContext"
+        scope="prototype"/>
+
+    <bean id="OpenIDVCIConsumeCredentialOffer" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ConsumeCredentialOffer"
+        scope="prototype" p:credentialOfferCache-ref="openidvci.CredentialOfferCache" />
+
+    <!-- REPLACED InitializeAuthenticationContext of oidc/abstract-api. -->
+    <bean id="InitializeAuthenticationContext" class="org.geant.shibboleth.plugin.openidvci.profile.impl.InitializeAuthenticationContext"
+        scope="prototype" />
+
+    <!-- ADDED. Error map of this plugin, extending the map of the OP. -->
+    <bean id="openidvci.token.MappedErrors" parent="shibboleth.oidc.DefaultApiMappedErrors"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map merge="true" value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+                <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).INVALID_TX_CODE}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_GRANT}" />
+                <entry key="#{T(org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds).NO_CREDENTIAL_OFFER}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_GRANT}" />
+                <entry key="#{T(org.opensaml.profile.action.EventIds).INVALID_PROFILE_CTX}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+                <entry key="#{T(org.opensaml.profile.action.EventIds).INVALID_MSG_CTX}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+                <entry key="#{T(org.opensaml.profile.action.EventIds).INVALID_SEC_CFG}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+                <entry key="#{T(org.opensaml.profile.action.EventIds).IO_ERROR}"
+                    value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+            </map>
+        </property>
+    </bean>
+
+    <!-- Condition signaling that the request was for the pre-authorized code grant. -->
+    <bean id="OpenIDVCIPreAuthorizedCodeGrantCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
+        p:grantTypes="#{T(org.geant.shibboleth.plugin.openidvci.messaging.impl.PreAuthorizedCodeGrant).GRANT_TYPE}" />
+
 </beans>
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 f36b0e5..aabc3fe 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
@@ -1,81 +1,301 @@
-<?xml version="1.0"?>
-<flow xmlns="http://www.springframework.org/schema/webflow" 
-      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
-      xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd" 
-      parent="openid/vci/abstract-api">
-
-  <action-state id="InitializeMandatoryContexts">
-    <evaluate expression="InitializeProfileRequestContext" />
-    <evaluate expression="PopulateMetricContext" />
-    <evaluate expression="FlowStartPopulateAuditContext" />
-    <evaluate expression="InitializeOutboundMessageContext" />
-    <evaluate expression="'proceed'" />
+<!--
+    Copy of the OpenID Connect Provider plugin's oidc/token flow applied to VCI
+    Idea is to merge OP token and this flow to one at some point in future.
+
+    ADDED
+      OpenIDVCIPreAuthorizedCodeGrantCondition     selects the pre-authorized code grant
+      OpenIDVCIValidateTxCode                      transaction code of a pre-authorized offer
+      OpenIDVCIValidatePreAuthorizedCode           one time use of a pre-authorized code
+      OpenIDVCISetAuthorizationDetailsToResponseContext   credential authorization details
+      OpenIDVCIConsumeCredentialOffer              retires the offer once the token is issued
+      OpenIDVCIBranchOnPreAuthorizedCodeGrant state   takes that grant aside
+      OpenIDVCIPreAuthzGrantProcessing state       the pre-authorized code grant branch
+      OpenIDVCIBuildTokensForCredentialsAccess state  the opaque access token of both grants
+
+    REPLACED
+      the parent of the flow   openid/vci/abstract-api in place of oidc/abstract-api
+      the bean import          the classpath form the other flows of this plugin use
+      UnwrapGrant              -> OpenIDVCIUnwrapGrant     also reads the credential offer cache
+
+    REPLACED, KEEPING THE ORIGINAL BEAN ID
+      DecodeMessage, FormOutboundMessage, InitializeAuthenticationContext
+          Named by states of openid/vci/abstract-api. Marked in token-beans.xml.
+
+    REMOVED
+      the transition from TraditionalGrantProcessing to CheckTraditionalGrantForAudience
+
+-->
+<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="openid/vci/abstract-api, oidc/metadata-lookup, oidc/consent-lookup">
+
+    <action-state id="InitializeMandatoryContexts">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="PopulateMetricContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="DecodeMessage">
+            <set name="flowScope.transitionAfterDecode" value="'PostDecodeMessage'" />
+         </transition>
+    </action-state>
+
+    <action-state id="PostDecodeMessage">
+        <!-- REPLACED UnwrapGrant. -->
+        <evaluate expression="OpenIDVCIUnwrapGrant" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
+        <transition on="proceed" to="DoMetadataLookup" />
+    </action-state>
+
+    <action-state id="SelectConfiguration">
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="PostLookupPopulateAuditContext" />
+        <evaluate expression="PopulateInboundInterceptContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckInboundInterceptContext" />
+    </action-state>
+
+    <!-- Authentication subflow happens here. -->
+
+    <action-state id="ResumeAfterAuthentication">
+        <evaluate expression="ValidateClientIDAgainstPolicy" />
+        <evaluate expression="ValidateGrantType" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="OpenIDVCIBranchOnPreAuthorizedCodeGrant" />
+    </action-state>
+
+    <!-- ADDED. Takes the pre-authorized code grant aside. -->
+    <decision-state id="OpenIDVCIBranchOnPreAuthorizedCodeGrant">
+        <if test="OpenIDVCIPreAuthorizedCodeGrantCondition.test(opensamlProfileRequestContext)"
+            then="OpenIDVCIPreAuthzGrantProcessing"
+            else="BranchOnGrantType" />
+    </decision-state>
+
+    <decision-state id="BranchOnGrantType">
+        <if test="NotClientCredentialsGrantCondition.test(opensamlProfileRequestContext)"
+            then="TraditionalGrantProcessing"
+            else="ClientCredentialsGrantProcessing" />
+    </decision-state>
+
+    <!-- These steps apply to grants that rely on the authorization endpoint to "prime" the token request. -->
+    <action-state id="TraditionalGrantProcessing">
+        <evaluate expression="ValidateGrant" />
+        <evaluate expression="ValidatePKCE" />
+        <evaluate expression="ValidateRedirectURI" />
+        <evaluate expression="SetRequestedClaimsToResponseContext" />
+        <evaluate expression="SetAuthenticationContextClassReferenceFromAuthzCodeToResponseContext" />
+        <evaluate expression="SetAuthenticationTimeFromAuthzCodeToResponseContext" />
+        <evaluate expression="SetTokenDeliveryAttributesFromTokenToResponseContext" />
+        <evaluate expression="SetConsentToResponseContext" />
+        <evaluate expression="InitializeSubjectContext" />
+        <evaluate expression="SetSubjectFromAuthzCodeToResponseContext" />
+        <evaluate expression="SetSectorIdentifierForAttributeResolution" />
+        <evaluate expression="ValidateScope" />
+        <evaluate expression="ValidateAudience" />
+        <evaluate expression="SetSessionIdFromAuthzCodeToResponseContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- REPLACED the transition to CheckTraditionalGrantForAudience. -->
+        <transition on="proceed" to="OpenIDVCIBuildTokensForCredentialsAccess" />
+    </action-state>
+
+    <!-- For standard grants, audience may or may not be a factor. -->
+    <decision-state id="CheckTraditionalGrantForAudience">
+        <if test="opensamlProfileRequestContext.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+            then="CheckAttributeResolutionForClient"
+            else="LookupAudienceMetadata" />
+    </decision-state>
+
+    <!-- ADDED. The pre-authorized code grant of OpenID4VCI. -->
+    <action-state id="OpenIDVCIPreAuthzGrantProcessing">
+        <evaluate expression="OpenIDVCIValidateTxCode" />
+        <evaluate expression="OpenIDVCIValidatePreAuthorizedCode" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="OpenIDVCIBuildTokensForCredentialsAccess" />
+    </action-state>
+
+    <!-- ADDED. Token building of both grants of this endpoint. -->
+    <action-state id="OpenIDVCIBuildTokensForCredentialsAccess">
+        <evaluate expression="OpenIDVCISetAuthorizationDetailsToResponseContext" />
+        <evaluate expression="BuildOIDCAccessToken" />
+        <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="OpenIDVCIConsumeCredentialOffer" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="BuildResponseMessage" />
+    </action-state>
+
+    <!-- These steps apply to grants that are self-contained on this endpoint. -->
+    <action-state id="ClientCredentialsGrantProcessing">
+        <evaluate expression="ValidateGrant" />
+        <evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
+        <evaluate expression="SetAuthenticationTimeToResponseContext" />
+        <evaluate expression="SetSubjectFromSubjectContextToResponseContext" />
+        <evaluate expression="SetSectorIdentifierForAttributeResolution" />
+        <evaluate expression="ValidateScope" />
+        <evaluate expression="ValidateAudience" />
+        <evaluate expression="SetSessionIdToResponseContext" />
+        
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="LookupAudienceMetadata" />
+    </action-state>
+
+    <!-- May need to add a second Relying Party for the primary resource/audience. -->
+
+    <action-state id="LookupAudienceMetadata">
+        <evaluate expression="AudienceOIDCMetadataLookup" />
+        <evaluate expression="InitializeAudienceRelyingPartyContext" />
+        <evaluate expression="'proceed'" />
+    
+        <transition on="proceed" to="CheckIfAudienceFoundFromClientInformationService" />
+    </action-state>
+    
+    <decision-state id="CheckIfAudienceFoundFromClientInformationService">
+        <if test="opensamlProfileRequestContext.ensureOutboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))"
+            then="SelectAudienceProfileConfiguration" else="LookupAudienceSAMLMetadata" />
+    </decision-state>
+    
+    <action-state id="LookupAudienceSAMLMetadata">
+        <evaluate expression="AudienceSAMLProtocolAndRole" />
+        <evaluate expression="SetAudienceEntityIdToSAMLPeerEntityContext" />
+        <evaluate expression="AudienceSAMLMetadataLookup" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="CheckIfAudienceFoundFromSAMLMetadata" />
+    </action-state>
+
+    <decision-state id="CheckIfAudienceFoundFromSAMLMetadata">
+        <if test="opensamlProfileRequestContext.ensureOutboundMessageContext().containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)) and opensamlProfileRequestContext.ensureOutboundMessageContext().ensureSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)).containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLMetadataContext))"
+            then="PopulateAudienceOIDCMetadataContextFromSAML"
+            else="SelectAudienceProfileConfiguration" />
+    </decision-state>
+    
+    <action-state id="PopulateAudienceOIDCMetadataContextFromSAML">
+        <evaluate expression="PopulateAudienceOIDCMetadataContext" />
+        <evaluate expression="InitializeAudienceRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="SelectAudienceProfileConfiguration" />
+    </action-state>
+
+    <action-state id="SelectAudienceProfileConfiguration">
+        <evaluate expression="SelectAudienceRelyingPartyConfiguration" />
+        <evaluate expression="SelectAudienceProfileConfiguration" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="CheckAttributeResolutionForAudience" />
+    </action-state>
+
+    <decision-state id="CheckAttributeResolutionForClient">
+        <if test="ResolveAttributesForClientPredicate.test(opensamlProfileRequestContext)"
+            then="AttributeResolutionForClient"
+            else="DoConsentLookup" />
+    </decision-state>
+
+    <action-state id="AttributeResolutionForClient">
+        <evaluate expression="ResolveAttributes" />
+        <evaluate expression="FilterAttributes" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="DoConsentLookup" />
+    </action-state>
+    
+    <decision-state id="CheckAttributeResolutionForAudience">
+        <if test="ResolveAttributesForAudiencePredicate.test(opensamlProfileRequestContext)"
+            then="AttributeResolutionForAudience"
+            else="DoConsentLookup" />
+    </decision-state>
+
+    <action-state id="AttributeResolutionForAudience">
+        <evaluate expression="ResolveAttributesForAudience" />
+        <evaluate expression="FilterAttributesForAudience" />
+        <evaluate expression="'proceed'" />
         
-    <transition on="proceed" to="DecodeMessage">
-      <set name="flowScope.transitionAfterDecode" value="'PostDecodeMessage'" />
-    </transition>
-  </action-state>
-
-  <action-state id="PostDecodeMessage">
-    <evaluate expression="UnwrapOrLocateGrant" />
-    <evaluate expression="'proceed'" />
-
-    <!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
-    <transition on="proceed" to="DoMetadataLookup" />
-  </action-state>
-  
-  <!-- Authentication subflow happens here. -->
-
-  <action-state id="ResumeAfterAuthentication">
-    <evaluate expression="'proceed'" />
-    <transition on="proceed" to="BranchOnGrantType">
-      <set name="flowScope.codeFlow" value="codeFlow.test(opensamlProfileRequestContext)" />
-    </transition>
-  </action-state>
-
-  <decision-state id="BranchOnGrantType">
-    <if test="codeFlow" then="TraditionalGrantProcessing" else="PreAuthzGrantProcessing" />
-  </decision-state>
-
-  <action-state id="TraditionalGrantProcessing">
-    <evaluate expression="ValidateExpectedGrantType"/>
-    <evaluate expression="ValidateGrant"/>
-    <evaluate expression="ValidatePKCE"/>
-    <evaluate expression="SetRequestedClaimsToResponseContext" />
-    <evaluate expression="SetAuthenticationContextClassReferenceFromAuthzCodeToResponseContext" />
-    <evaluate expression="SetAuthenticationTimeFromAuthzCodeToResponseContext" />
-    <evaluate expression="SetTokenDeliveryAttributesFromTokenToResponseContext" />
-    <evaluate expression="SetConsentToResponseContext" />
-    <evaluate expression="InitializeSubjectContext" />
-    <evaluate expression="SetSubjectFromAuthzCodeToResponseContext" />
+        <transition on="proceed" to="DoConsentLookup" />
+    </action-state>
+    
+    <decision-state id="BuildResponse">
+        <if test="opensamlProfileRequestContext.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+            then="BuildTokensForUserInfoAccess"
+            else="BuildTokensForThirdPartyAccess" />
+    </decision-state>
+    
+    <!--
+    Note no JWT encryption here. The token shouldn't even be a JWT, but even if it is,
+    the only audience is us, and the client can't be expected to decrypt it, so it
+    wouldn't be usable. If it were encrypted to our key then there would be no point to
+    allowing it to be a JWT.
+    
+    Note also no attribute claims are added to the access token since that isn't a proper
+    delivery mechanism for claims to the OIDC client.
+    -->
+    <action-state id="BuildTokensForUserInfoAccess">
+        <evaluate expression="PopulateUserInfoAccessTokenSignatureSigningParameters" />
+        <evaluate expression="BuildOIDCAccessToken" />
+        <evaluate expression="SignOIDCAccessToken" />
+        <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="SetRefreshTokenToResponseContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="ProducingIDToken" />
+    </action-state>
+
+    <!--
     
-    <evaluate expression="'proceed'"/>
-    <transition on="proceed" to="PostValidation"/>
-  </action-state>
-
-  <action-state id="PreAuthzGrantProcessing">
-    <evaluate expression="ValidateExpectedGrantType"/>
-    <evaluate expression="ValidateTxCode"/>
-    <evaluate expression="ValidatePreAuthorizedCode"/>
-    <evaluate expression="'proceed'"/>
-    <transition on="proceed" to="PostValidation"/>
-  </action-state>
-  
-  <action-state id="PostValidation">
-  <evaluate expression="SetAuthorizationDetailsToResponseContext"/>
-    <evaluate expression="'proceed'"/>
-    <transition on="proceed" to="BuildTokensForCredentialsAccess"/>
-  </action-state>
-
-  <action-state id="BuildTokensForCredentialsAccess">
-    <evaluate expression="BuildAccessToken" />
-    <evaluate expression="SetOAuthAccessTokenToResponseContext" />
-    <evaluate expression="ConsumeCredentialOffer" />
-    <evaluate expression="'proceed'" />
-    <transition on="proceed" to="BuildResponseMessage" />
-  </action-state>
-
-
-  <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-beans.xml"/>
+    This includes OIDC and OAuth use cases and all grant types but the primary audience
+    for the access token is the resource/audience. The OP MAY also be an audience for the
+    token if the "oidc" scope is in play, in which case the actions have to behave differently
+    or be disabled in some cases. Encryption is for example impossible in that case, as with
+    the OIDC-only use of JWTs above.
+    -->
+    <action-state id="BuildTokensForThirdPartyAccess">
+        <evaluate expression="PopulateThirdPartyAccessTokenSignatureSigningParameters" />
+        <evaluate expression="PopulateThirdPartyAccessTokenEncryptionParameters" />
+        <evaluate expression="AddAttributeClaimsToAccessToken" />
+        <evaluate expression="BuildAccessToken" />
+        <evaluate expression="SignAccessToken" />
+        <evaluate expression="EncryptAccessToken" />
+        <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+        <evaluate expression="SetRefreshTokenToResponseContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="ProducingIDToken" />
+    </action-state>
+
+    <!-- ID token only issued when scope includes "openid" -->
+
+    <decision-state id="ProducingIDToken">
+        <if test="IssueIDTokenCondition.test(opensamlProfileRequestContext)"
+            then="BuildIDToken"
+            else="BuildResponseMessage" />
+    </decision-state>
+
+    <action-state id="BuildIDToken">
+        <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
+        <evaluate expression="PopulateIDTokenEncryptionParameters" />
+        <evaluate expression="AddIDTokenShell" />
+        <evaluate expression="AddAttributeClaimsToIDToken" />
+        <evaluate expression="AddTokenDeliveryAttributesToIDToken" />
+        <evaluate expression="AddAuthTimeToIDToken" />
+        <evaluate expression="AddAcrToIDToken" />
+        <evaluate expression="AddNonceToIDToken" />
+        <evaluate expression="AddAccessTokenHashToIDToken" />
+        <evaluate expression="ManipulateClaimsForIDToken" />
+        <evaluate expression="SignIDToken" />
+        <evaluate expression="EncryptIDToken" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="BuildResponseMessage" />
+    </action-state>
+
+    <!-- REPLACED the relative import with the classpath form. -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-beans.xml" />
 
 </flow>
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 0002459..be5cdb8 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
@@ -25,6 +25,8 @@ import org.testng.annotations.Test;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.GrantType;
 import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
 import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
@@ -60,46 +62,49 @@ public class OpenIDVCITokenRequestTest {
         Assert.assertEquals("http://example.com", message.getEndpointURI().toString());
         Assert.assertEquals("txCodeValue", message.getTxCode());
         Assert.assertEquals("CodeVerifier", message.getCodeVerifier());
-        Assert.assertEquals(message.getAuthorizationDetails().size(), 1);
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getType(), "openid_credential");
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getCredentialConfigurationId(),
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().size(), 1);
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getType(), "openid_credential");
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getCredentialConfigurationId(),
                 "UniversityDegreeCredential");
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getClaims().size(), 3);
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getClaims().get(0).getPath().get(0),
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getClaims().size(), 3);
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getClaims().get(0).getPath().get(0),
                 "credentialSubject");
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getClaims().get(0).getPath().get(1), "given_name");
-        Assert.assertTrue(message.getAuthorizationDetails().get(0).getClaims().get(0).getMandatory());
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getClaims().get(1).getPath().get(0),
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getClaims().get(0).getPath().get(1),
+                "given_name");
+        Assert.assertTrue(message.getCredentialAuthorizationDetails().get(0).getClaims().get(0).getMandatory());
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getClaims().get(1).getPath().get(0),
                 "credentialSubject");
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getClaims().get(1).getPath().get(1),
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getClaims().get(1).getPath().get(1),
                 "family_name");
-        Assert.assertFalse(message.getAuthorizationDetails().get(0).getClaims().get(1).getMandatory());
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getClaims().get(2).getPath().get(0),
+        Assert.assertFalse(message.getCredentialAuthorizationDetails().get(0).getClaims().get(1).getMandatory());
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getClaims().get(2).getPath().get(0),
                 "credentialSubject");
-        Assert.assertEquals(message.getAuthorizationDetails().get(0).getClaims().get(2).getPath().get(1), "degree");
-        Assert.assertFalse(message.getAuthorizationDetails().get(0).getClaims().get(2).getMandatory());
-        // If this step fails we can adopt Nimbus token request class as our main
-        // message class.
-        Assert.assertNull(message.getOPTokenRequest());
+        Assert.assertEquals(message.getCredentialAuthorizationDetails().get(0).getClaims().get(2).getPath().get(1),
+                "degree");
+        Assert.assertFalse(message.getCredentialAuthorizationDetails().get(0).getClaims().get(2).getMandatory());
+        Assert.assertTrue(message.getAuthorizationGrant() instanceof PreAuthorizedCodeGrant);
+        Assert.assertEquals(message.getAuthorizationGrant().getType(), PreAuthorizedCodeGrant.GRANT_TYPE);
+        Assert.assertEquals(((PreAuthorizedCodeGrant) message.getAuthorizationGrant()).getPreAuthorizedCode(),
+                "123456");
     }
 
     // @Test(expectedExceptions = IllegalArgumentException.class)
     public void testInvalidGrantType() throws MessageDecodingException, URISyntaxException {
         message = new OpenIDVCITokenRequest(new URI("http://example.com"), new ClientID("clientID"), "none", "123456",
-                null, null, null, null, null);
+                null, null, null, null);
     }
 
     // @Test(expectedExceptions = IllegalArgumentException.class)
     public void testMissingCode() throws MessageDecodingException, URISyntaxException {
         message = new OpenIDVCITokenRequest(new URI("http://example.com"), new ClientID("clientID"),
-                OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH, null, null, null, null, null, null);
+                OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH, null, null, null, null, null);
     }
 
     // @Test
     public void testClientAuthnGetters() throws MessageDecodingException, ParseException, URISyntaxException {
         ClientAuthentication clientAuth = new ClientSecretBasic(new ClientID("clientID"), new Secret());
         message = new OpenIDVCITokenRequest(new URI("http://example.com"), clientAuth,
-                OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH, "123456", null, null, null, null, null);
+                OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH, "123456", null, null, null, null);
         Assert.assertNull(message.getClientID());
         Assert.assertEquals("123456", message.getPreAuthorizedCode());
         Assert.assertEquals(OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH, message.getGrantType());
@@ -114,7 +119,8 @@ public class OpenIDVCITokenRequestTest {
         httpRequest.setQuery(
                 "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.assertNotNull(message.getOPTokenRequest());
+        Assert.assertTrue(message.getAuthorizationGrant() instanceof AuthorizationCodeGrant);
+        Assert.assertEquals(message.getAuthorizationGrant().getType(), GrantType.AUTHORIZATION_CODE);
     }
 
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCITokenConfigurationTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCITokenConfigurationTest.java
new file mode 100644
index 0000000..1acdfea
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCITokenConfigurationTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.config.impl;
+
+import java.util.List;
+import java.util.Set;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCITokenRequest;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenConfiguration;
+
+/** Tests for {@link DefaultOpenIDVCITokenConfiguration}. */
+public class DefaultOpenIDVCITokenConfigurationTest {
+
+    private DefaultOpenIDVCITokenConfiguration configuration;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        configuration = new DefaultOpenIDVCITokenConfiguration();
+    }
+
+    @Test
+    public void testIsOAuth2TokenConfiguration() {
+        Assert.assertTrue(configuration instanceof OAuth2TokenConfiguration);
+    }
+
+    @Test
+    public void testDefaultGrantTypes() {
+        Assert.assertEquals(configuration.getGrantTypes(null),
+                Set.of("authorization_code", OpenIDVCITokenRequest.GRANT_TYPE_VALUE_PRE_AUTH));
+    }
+
+    @Test
+    public void testSetGrantTypes() {
+        configuration.setGrantTypes(List.of("authorization_code"));
+        Assert.assertEquals(configuration.getGrantTypes(null), Set.of("authorization_code"));
+    }
+
+    @Test
+    public void testSetGrantTypesLookupStrategy() {
+        configuration.setGrantTypesLookupStrategy(prc -> Set.of("a", "b", "c"));
+        Assert.assertEquals(configuration.getGrantTypes(null).size(), 3);
+    }
+
+    @Test
+    public void testRefreshTokensDisabled() {
+        Assert.assertFalse(configuration.isRefreshTokensEnabled(null));
+    }
+
+    @Test
+    public void testFixedRefreshTokenAndAudienceSettings() {
+        Assert.assertFalse(configuration.isEnforceRefreshTokenRotation(null));
+        Assert.assertNull(configuration.getRefreshTokenClaimsSetManipulationStrategy(null));
+        Assert.assertFalse(configuration.isIssueIdTokenViaRefreshToken(null));
+        Assert.assertFalse(configuration.isLimitInitialAccessTokenToSelf(null));
+    }
+
+}

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


More information about the commits mailing list