[java-idp-plugin-vci] 01/05: Improve access token validation as per now that we support also jwt tokens in theory

Codeberg noreply at shibboleth.net
Mon Sep 28 15:53:42 UTC 2026


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

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

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

commit 8a55269bf57a7987423775c41c5652f0bb9de65d
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Mon Sep 28 17:11:04 2026 +0300

    Improve access token validation as per now that we support also jwt tokens in theory
---
 README.md                                          |   7 +-
 .../DefaultOpenIDVCICredentialsConfiguration.java  |  45 ++++-
 .../SetCredentialsAudienceToResponseContext.java   |  56 ++++++
 .../profile/impl/ValidateAccessTokenBinding.java   |  63 ++++++-
 .../idp/flows/openid/vci/token/token-beans.xml     |   4 +
 .../idp/flows/openid/vci/token/token-flow.xml      |   2 +
 .../idp/service/relying-party/postconfig.xml       |  31 +++-
 ...etCredentialsAudienceToResponseContextTest.java |  91 ++++++++++
 .../impl/ValidateAccessTokenBindingTest.java       | 201 +++++++++++++++++++++
 9 files changed, 494 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index 30ee58e..b6fa79d 100644
--- a/README.md
+++ b/README.md
@@ -719,7 +719,9 @@ in the same *metadata/oidc-client.json* as the clients above:
 
 Note the `scope` value. Every configuration id you want to be requestable by scope needs to be
 here. `grant_types` has the pre-authorized code grant as well, as the same wallet is usually
-also the one that redeems an offer.
+also the one that redeems an offer. `audience` needs `credentials` for wallets that request
+without `openid` in `scope`, the authorization endpoint rejects those when the client has no
+audience.
 
 The relying party override needs four profiles, authorize, token, credential and pushed
 authorization request:
@@ -1942,6 +1944,7 @@ Endpoint `/idp/profile/openid/vci/credentials`, profile ID `http://geant.org/ns/
 | `credentialSignatureSigningConfiguration` | SignatureSigningConfiguration | **openidvci.SigningConfiguration** | Signing of credentials. |
 | `proofSignatureValidationConfiguration` | SignatureValidationConfiguration | **openidvci.ProofSignatureValidationConfiguration** | Validation of key proof signatures. |
 | `proofClaimsValidator` | ClaimsValidator | **openidvci.DefaultProofBodyClaimsValidator** | Validation of key proof claims. |
+| `issuedClaimsValidator` | ClaimsValidator | **openidvci.DefaultAccessTokenClaimsValidator** | Validation of access token claims. |
 | `proofNonceGenerator` | Function<ProfileRequestContext,String> | **openidvci.DefaultOpenIDVCINonceGenerator** | Nonce a key proof has to carry. Same generator as in **OpenID.VCI.Nonce**. `#{null}` in both for no nonce. |
 | `batchSize` | Integer | `2` | Largest number of key proofs in one request. Not read when the Credential Issuer metadata has `batch_credential_issuance`, its `batch_size` is used. Property `openidvci.batchSize`. |
 | `keyAttestationClaimsValidator` | ClaimsValidator | bean **openidvci.KeyAttestationClaimsValidator** if defined | Validation of key attestation claims, `nonce` among them. |
@@ -2076,6 +2079,8 @@ Beans you may define or refer to. Define your own in *conf/global.xml* or in any
 | **openidvci.ProofSignatureValidationConfiguration** | SignatureValidationConfiguration | built in | Validation of key proof signatures. |
 | **openidvci.DefaultProofBodyClaimsValidator** | ClaimsValidator | built in | Validation of key proof claims, `aud` against the published `credential_issuer`. |
 | **openidvci.DefaultOpenIDVCINonceGenerator** | Function | built in | Nonce of key proofs. |
+| **openidvci.DefaultAccessTokenClaimsValidator** | ClaimsValidator | built in | Validation of access token claims at the credential endpoint. Chains **openidvci.AccessTokenClaimsValidators**. |
+| **openidvci.AccessTokenClaimsValidators** | List<ClaimsValidator> | built in | `jti`, expiry, not before, `iss`, `client_id`, `aud` containing `credentials`, the claims only this plugin's token endpoint writes, and revocation. A list of your own needs to keep **openidvci.CredentialsAudienceClaimsValidator** and **openidvci.RequiredAccessTokenClaimsValidator**, or access tokens of the OP are accepted. |
 | **openidvci.KeyAttestationTrustAnchors** | List<Credential> | | Certificates key attestations are accepted under. Needed for `key_attestations_required`. |
 | **openidvci.KeyAttestationClaimsValidator** | ClaimsValidator | | Validation of key attestation claims, `nonce` among them. |
 | **openidvci.ClientAttestationValidator** | CredentialValidator | built in | Authenticates a wallet with `OAuth-Client-Attestation` headers. Add to **shibboleth.authn.OAuth2Client.Validators** before **openidvci.PublicClientValidator**. |
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCICredentialsConfiguration.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCICredentialsConfiguration.java
index 1910aa7..c13bc99 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCICredentialsConfiguration.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultOpenIDVCICredentialsConfiguration.java
@@ -16,14 +16,57 @@
 
 package org.geant.shibboleth.plugin.openidvci.profile.config.impl;
 
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import org.geant.shibboleth.plugin.openidvci.profile.config.OpenIDVCIConfiguration;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenValidatingProfileConfiguration;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
 
 /** Profile configuration for the Credential endpoint. */
-public class DefaultOpenIDVCICredentialsConfiguration extends AbstractOpenIDVCIConfiguration{
+public class DefaultOpenIDVCICredentialsConfiguration extends AbstractOpenIDVCIConfiguration
+        implements OAuth2TokenValidatingProfileConfiguration {
+
+    /** Validation of JWT claims for issued tokens. */
+    @Nonnull
+    private Function<ProfileRequestContext, ClaimsValidator> issuedClaimsValidatorLookupStrategy;
 
     /** Constructor. */
     public DefaultOpenIDVCICredentialsConfiguration() {
         super(OpenIDVCIConfiguration.PROFILE_ID_CREDENTIALS);
+        issuedClaimsValidatorLookupStrategy = FunctionSupport.constant(null);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public ClaimsValidator getIssuedClaimsValidator(@Nullable final ProfileRequestContext profileRequestContext) {
+        return issuedClaimsValidatorLookupStrategy.apply(profileRequestContext);
+    }
+
+    /**
+     * Set the {@link ClaimsValidator} to apply to issued access tokens.
+     *
+     * @param validator validator to use
+     */
+    public void setIssuedClaimsValidator(@Nullable final ClaimsValidator validator) {
+        issuedClaimsValidatorLookupStrategy = FunctionSupport.constant(validator);
+    }
+
+    /**
+     * Set a lookup strategy for the {@link ClaimsValidator} to apply to issued access tokens.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setIssuedClaimsValidatorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, ClaimsValidator> strategy) {
+        issuedClaimsValidatorLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
 
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetCredentialsAudienceToResponseContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetCredentialsAudienceToResponseContext.java
new file mode 100644
index 0000000..5e24716
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetCredentialsAudienceToResponseContext.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.profile.impl;
+
+import javax.annotation.Nonnull;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.AbstractOpenIDVCITokenResponseAction;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that adds the audience of the Credential endpoint to {@link OIDCAuthenticationResponseContext}.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ */
+public class SetCredentialsAudienceToResponseContext extends AbstractOpenIDVCITokenResponseAction {
+
+    /** Audience of the Credential endpoint. */
+    @Nonnull
+    public static final String AUDIENCE = "credentials";
+
+    /** Class logger. */
+    @Nonnull
+    private Logger log = LoggerFactory.getLogger(SetCredentialsAudienceToResponseContext.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+        assert oidcResponseContext != null;
+        if (!oidcResponseContext.getAudience().contains(AUDIENCE)) {
+            oidcResponseContext.getAudience().add(AUDIENCE);
+        }
+        log.debug("{} Access token audience {}", getLogPrefix(), oidcResponseContext.getAudience());
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBinding.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBinding.java
index d4f6b35..00ed678 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBinding.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBinding.java
@@ -18,6 +18,7 @@ package org.geant.shibboleth.plugin.openidvci.profile.impl;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.AbstractOpenIDVCICredentialsValidationResponseAction;
 import org.opensaml.profile.action.ActionSupport;
@@ -25,16 +26,24 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.nimbusds.jwt.JWTClaimsSet;
+
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultDPoPProofThumbprintLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.profile.config.navigate.IssuedClaimsValidatorLookupFunction;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
  * Action that validates the access token of the request to be bound to the key the DPoP proof of
- * the request is signed with, as required by RFC 9449 section 7.1.
+ * the request is signed with, as required by RFC 9449 section 7.1, and validates the claims of the
+ * access token with the issued claims validator of the profile configuration.
  *
  * <p>Runs after DPoP proof validation, which is what makes the thumbprint of the proof key
  * available.</p>
@@ -42,6 +51,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link OidcEventIds#INVALID_ACCESS_TOKEN}
  * @event {@link OidcEventIds#INVALID_DPOP_PROOF}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
  */
 public class ValidateAccessTokenBinding extends AbstractOpenIDVCICredentialsValidationResponseAction {
 
@@ -54,6 +64,15 @@ public class ValidateAccessTokenBinding extends AbstractOpenIDVCICredentialsVali
     private Function<ProfileRequestContext, String> dpopProofThumbprintLookupStrategy =
             new DefaultDPoPProofThumbprintLookupFunction();
 
+    /** Strategy used to locate the issued claims validator. */
+    @Nonnull
+    private Function<ProfileRequestContext, ClaimsValidator> claimsValidatorLookupStrategy =
+            new IssuedClaimsValidatorLookupFunction();
+
+    /** Issued claims validator. */
+    @Nullable
+    private ClaimsValidator claimsValidator;
+
     /**
      * Set the strategy used to read the thumbprint of the validated DPoP proof.
      *
@@ -66,6 +85,33 @@ public class ValidateAccessTokenBinding extends AbstractOpenIDVCICredentialsVali
         dpopProofThumbprintLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
 
+    /**
+     * Set the strategy used to locate the issued claims validator.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setClaimsValidatorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, ClaimsValidator> strategy) {
+        checkSetterPreconditions();
+
+        claimsValidatorLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        claimsValidator = claimsValidatorLookupStrategy.apply(profileRequestContext);
+        if (claimsValidator == null) {
+            log.error("{} Unable to obtain ClaimsValidator to apply", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        return true;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -73,7 +119,7 @@ public class ValidateAccessTokenBinding extends AbstractOpenIDVCICredentialsVali
         final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
         assert oidcResponseContext != null;
         final TokenClaimsSet tokenClaims = oidcResponseContext.getAuthorizationGrantClaimsSet();
-        if (tokenClaims == null) {
+        if (!(tokenClaims instanceof AccessTokenClaimsSet) || tokenClaims.getClaimsSet() == null) {
             log.error("{} No access token claims set to validate", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
             return;
@@ -91,7 +137,18 @@ public class ValidateAccessTokenBinding extends AbstractOpenIDVCICredentialsVali
             }
         }
 
-        log.debug("{} Access token {} is bound to the key of the DPoP proof", getLogPrefix(), tokenClaims.getID());
+        final JWTClaimsSet claimsSet = tokenClaims.getClaimsSet();
+        assert claimsSet != null;
+        try {
+            assert claimsValidator != null;
+            claimsValidator.validate(claimsSet, profileRequestContext);
+        } catch (final JWTValidationException e) {
+            log.warn("{} Access token {} is invalid: {}", getLogPrefix(), tokenClaims.getID(), e.getMessage());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
+            return;
+        }
+
+        log.debug("{} Access token {} validated", getLogPrefix(), tokenClaims.getID());
     }
 
 }
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 bf57af6..1484af8 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
@@ -855,6 +855,10 @@
         class="org.geant.shibboleth.plugin.openidvci.profile.impl.SetDPoPProofThumbprintToResponseContext"
         scope="prototype" />
 
+    <bean id="OpenIDVCISetCredentialsAudienceToResponseContext"
+        class="org.geant.shibboleth.plugin.openidvci.profile.impl.SetCredentialsAudienceToResponseContext"
+        scope="prototype" />
+
     <!-- REPLACED InitializeAuthenticationContext of oidc/abstract-api. -->
     <bean id="InitializeAuthenticationContext" class="org.geant.shibboleth.plugin.openidvci.profile.impl.InitializeAuthenticationContext"
         scope="prototype" />
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/token/token-flow.xml
index 3db0498..44a1175 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
@@ -9,6 +9,7 @@
       OpenIDVCISetAuthorizationDetailsToResponseContext   credential authorization details
       OpenIDVCIConsumeCredentialOffer              retires a stored offer before the transaction code is read
       OpenIDVCISetDPoPProofThumbprintToResponseContext   DPoP key the pre-authorized access token is bound to
+      OpenIDVCISetCredentialsAudienceToResponseContext   audience of the Credential endpoint in the access token of both grants
       OpenIDVCIFilterPotentialCredentials          credentials of the code grant limited to the granted scope and authorization_details
       OpenIDVCIBranchOnPreAuthorizedCodeGrant state   takes that grant aside
       OpenIDVCIPreAuthzGrantProcessing state       the pre-authorized code grant branch
@@ -130,6 +131,7 @@
     <!-- ADDED. Token building of both grants of this endpoint. -->
     <action-state id="OpenIDVCIBuildTokensForCredentialsAccess">
         <evaluate expression="OpenIDVCISetAuthorizationDetailsToResponseContext" />
+        <evaluate expression="OpenIDVCISetCredentialsAudienceToResponseContext" />
         <evaluate expression="BuildOIDCAccessToken" />
         <evaluate expression="SetOAuthAccessTokenToResponseContext" />
         <evaluate expression="'proceed'" />
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 62b5df2..70d07d6 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -48,7 +48,8 @@
         class="org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultOpenIDVCINonceConfiguration" />
         
     <bean id="OpenID.VCI.Credentials" parent="AbstractVCIProfile" lazy-init="true"
-        class="org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultOpenIDVCICredentialsConfiguration" />
+        class="org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultOpenIDVCICredentialsConfiguration"
+        p:issuedClaimsValidator="#{getObject('openidvci.DefaultAccessTokenClaimsValidator')}" />
 
     <!--
     Endpoint publishing Credential Issuer metadata. Serves wallets before there is a
@@ -109,6 +110,34 @@
         class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
         p:claimValidators-ref="openidvci.ProofBodyClaimsValidator" />
 
+    <bean id="openidvci.DefaultAccessTokenClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="openidvci.AccessTokenClaimsValidators" />
+
+    <util:list id="openidvci.AccessTokenClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="RequiredClaimsValidator" />
+        <ref bean="openidvci.RequiredAccessTokenClaimsValidator" />
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+        <ref bean="SelfIssuedClaimsValidator" />
+        <ref bean="ClientIDClaimsValidator" />
+        <ref bean="openidvci.CredentialsAudienceClaimsValidator" />
+        <ref bean="JWTIDRevocationClaimsValidator" />
+        <ref bean="RootJWTIDRevocationClaimsValidator" />
+        <ref bean="TokenRevocationConditionValidator" />
+    </util:list>
+
+    <bean id="openidvci.RequiredAccessTokenClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+        p:requiredClaims="#{T(org.geant.shibboleth.plugin.openidvci.profile.logic.OpenIDVCITokenManipulationStrategy).POTENTIAL_CLAIMS}" />
+
+    <bean id="openidvci.CredentialsAudienceClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator">
+        <property name="audienceLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression" c:expression="T(org.geant.shibboleth.plugin.openidvci.profile.impl.SetCredentialsAudienceToResponseContext).AUDIENCE" />
+        </property>
+    </bean>
+
     <bean id="openidvci.ProofNonceGeneratorLookupFunction"
         class="org.geant.shibboleth.plugin.openidvci.profile.config.navigate.ProofNonceGeneratorLookupFunction" />    
 
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetCredentialsAudienceToResponseContextTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetCredentialsAudienceToResponseContextTest.java
new file mode 100644
index 0000000..9d72d12
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SetCredentialsAudienceToResponseContextTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.profile.impl;
+
+import java.util.List;
+
+import org.geant.shibboleth.plugin.openidvci.decoding.impl.OpenIDVCITokenRequestDecoder;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Unit tests for {@link SetCredentialsAudienceToResponseContext}.
+ */
+public class SetCredentialsAudienceToResponseContextTest {
+
+    private RequestContext requestCtx;
+
+    private OIDCAuthenticationResponseContext responseCtx;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        final MockHttpServletRequest httpRequest = new MockHttpServletRequest();
+        httpRequest.setMethod("POST");
+        httpRequest.addHeader("Content-Type", "application/x-www-form-urlencoded");
+        httpRequest.addParameter("client_id", "123456");
+        httpRequest.addParameter("pre-authorized_code", "123456");
+        httpRequest.addParameter("grant_type", "urn:ietf:params:oauth:grant-type:pre-authorized_code");
+        final OpenIDVCITokenRequestDecoder decoder = new OpenIDVCITokenRequestDecoder();
+        decoder.setHttpServletRequestSupplier(new NonnullSupplier<>() {
+            public HttpServletRequest get() {
+                return httpRequest;
+            }
+        });
+        decoder.initialize();
+        decoder.decode();
+
+        requestCtx = new RequestContextBuilder().buildRequestContext();
+        final ProfileRequestContext profileRequestCtx =
+                new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+        profileRequestCtx.setInboundMessageContext(decoder.getMessageContext());
+        profileRequestCtx.setOutboundMessageContext(new MessageContext());
+        responseCtx = profileRequestCtx.getOutboundMessageContext()
+                .ensureSubcontext(OIDCAuthenticationResponseContext.class);
+    }
+
+    @Test
+    public void testAdded() throws Exception {
+        final SetCredentialsAudienceToResponseContext action = new SetCredentialsAudienceToResponseContext();
+        action.initialize();
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        Assert.assertEquals(responseCtx.getAudience(), List.of(SetCredentialsAudienceToResponseContext.AUDIENCE));
+    }
+
+    @Test
+    public void testNotDuplicated() throws Exception {
+        responseCtx.getAudience().add(SetCredentialsAudienceToResponseContext.AUDIENCE);
+        responseCtx.getAudience().add("other");
+        final SetCredentialsAudienceToResponseContext action = new SetCredentialsAudienceToResponseContext();
+        action.initialize();
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        Assert.assertEquals(responseCtx.getAudience(),
+                List.of(SetCredentialsAudienceToResponseContext.AUDIENCE, "other"));
+    }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBindingTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBindingTest.java
new file mode 100644
index 0000000..787de6a
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBindingTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.profile.impl;
+
+import java.net.URI;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCICredentialsRequest;
+import org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultOpenIDVCICredentialsConfiguration;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierRevocationValidator;
+import net.shibboleth.profile.context.RelyingPartyContext;
+
+/**
+ * Unit tests for {@link ValidateAccessTokenBinding}.
+ */
+public class ValidateAccessTokenBindingTest {
+
+    private static final String REVOCATION_CONTEXT = "test";
+
+    private RequestContext requestCtx;
+
+    private ProfileRequestContext profileRequestCtx;
+
+    private DefaultOpenIDVCICredentialsConfiguration configuration;
+
+    private Set<String> revoked;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        requestCtx = new RequestContextBuilder().buildRequestContext();
+        profileRequestCtx = new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+        profileRequestCtx.setInboundMessageContext(new MessageContext());
+        profileRequestCtx.setOutboundMessageContext(new MessageContext());
+        final HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+        httpRequest.setAuthorization("Bearer dGVzdDp0ZXN0");
+        httpRequest.setContentType("application/json");
+        httpRequest.setBody("{\"credential_configuration_id\":\"GeantIncubatorDiploma\"}");
+        profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+        profileRequestCtx.getOutboundMessageContext().ensureSubcontext(OIDCAuthenticationResponseContext.class);
+        revoked = new HashSet<>();
+        configuration = new DefaultOpenIDVCICredentialsConfiguration();
+        configuration.setIssuedClaimsValidator(validator());
+        profileRequestCtx.ensureSubcontext(RelyingPartyContext.class).setProfileConfig(configuration);
+    }
+
+    private ClaimsValidator validator() throws Exception {
+        final ExpiryClaimsValidator expiry = new ExpiryClaimsValidator();
+        expiry.setId("expiry");
+        expiry.setClockSkew(Duration.ZERO);
+        expiry.initialize();
+        final JWTIdentifierRevocationValidator revocation = new JWTIdentifierRevocationValidator();
+        revocation.setId("revocation");
+        revocation.setRevocationCache(new SetRevocationCache());
+        revocation.setContext(REVOCATION_CONTEXT);
+        revocation.initialize();
+        final ChainingJWTClaimsValidator chain = new ChainingJWTClaimsValidator();
+        chain.setId("chain");
+        chain.setClaimValidators(List.of(expiry, revocation));
+        chain.initialize();
+        return chain;
+    }
+
+    private void setToken(final Instant expiresAt, final String thumbprint) {
+        final AccessTokenClaimsSet.Builder builder = new AccessTokenClaimsSet.Builder();
+        builder.setJWTID("jti").setClientID(new ClientID("client")).setIssuer("issuer").setPrincipal("principal")
+                .setSubject("subject").setIssuedAt(Instant.now().minusSeconds(120)).setExpiresAt(expiresAt)
+                .setAuthenticationTime(Instant.now().minusSeconds(120)).setDpopProofJwkThumbprint(thumbprint);
+        profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class)
+                .setAuthorizationGrantClaimsSet(builder.build());
+    }
+
+    private ValidateAccessTokenBinding actionOf(final String proofThumbprint) throws Exception {
+        final ValidateAccessTokenBinding action = new ValidateAccessTokenBinding();
+        action.setDpopProofThumbprintLookupStrategy(prc -> proofThumbprint);
+        action.initialize();
+        return action;
+    }
+
+    @Test
+    public void testUnbound() throws Exception {
+        setToken(Instant.now().plusSeconds(60), null);
+        ActionTestingSupport.assertProceedEvent(actionOf(null).execute(requestCtx));
+    }
+
+    @Test
+    public void testBound() throws Exception {
+        setToken(Instant.now().plusSeconds(60), "jkt");
+        ActionTestingSupport.assertProceedEvent(actionOf("jkt").execute(requestCtx));
+    }
+
+    @Test
+    public void testBoundOtherKey() throws Exception {
+        setToken(Instant.now().plusSeconds(60), "jkt");
+        ActionTestingSupport.assertEvent(actionOf("other").execute(requestCtx), OidcEventIds.INVALID_DPOP_PROOF);
+    }
+
+    @Test
+    public void testBoundNoProof() throws Exception {
+        setToken(Instant.now().plusSeconds(60), "jkt");
+        ActionTestingSupport.assertEvent(actionOf(null).execute(requestCtx), OidcEventIds.INVALID_DPOP_PROOF);
+    }
+
+    @Test
+    public void testUnboundWithProof() throws Exception {
+        setToken(Instant.now().plusSeconds(60), null);
+        ActionTestingSupport.assertEvent(actionOf("jkt").execute(requestCtx), OidcEventIds.INVALID_DPOP_PROOF);
+    }
+
+    @Test
+    public void testExpired() throws Exception {
+        setToken(Instant.now().minusSeconds(60), null);
+        ActionTestingSupport.assertEvent(actionOf(null).execute(requestCtx), OidcEventIds.INVALID_ACCESS_TOKEN);
+    }
+
+    @Test
+    public void testRevoked() throws Exception {
+        setToken(Instant.now().plusSeconds(60), null);
+        revoked.add("jti");
+        ActionTestingSupport.assertEvent(actionOf(null).execute(requestCtx), OidcEventIds.INVALID_ACCESS_TOKEN);
+    }
+
+    @Test
+    public void testNoToken() throws Exception {
+        ActionTestingSupport.assertEvent(actionOf(null).execute(requestCtx), OidcEventIds.INVALID_ACCESS_TOKEN);
+    }
+
+    @Test
+    public void testNoValidator() throws Exception {
+        configuration.setIssuedClaimsValidator(null);
+        setToken(Instant.now().plusSeconds(60), null);
+        ActionTestingSupport.assertEvent(actionOf(null).execute(requestCtx), IdPEventIds.INVALID_PROFILE_CONFIG);
+    }
+
+    private class SetRevocationCache implements RevocationCache {
+
+        @Override
+        public boolean revoke(final String context, final String key, final String value) {
+            return revoked.add(key);
+        }
+
+        @Override
+        public boolean revoke(final String context, final String key, final String value, final Duration exp) {
+            return revoked.add(key);
+        }
+
+        @Override
+        public boolean unrevoke(final String context, final String key) {
+            return revoked.remove(key);
+        }
+
+        @Override
+        public boolean isRevoked(final String context, final String key) {
+            return REVOCATION_CONTEXT.equals(context) && revoked.contains(key);
+        }
+
+        @Override
+        public String getRevocationRecord(final String context, final String key) {
+            return isRevoked(context, key) ? "y" : null;
+        }
+
+    }
+
+}

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


More information about the commits mailing list