[java-idp-plugin-vci] 02/07: Validate access token binding

Codeberg noreply at shibboleth.net
Thu Sep 24 13:16:27 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-idp-plugin-vci.

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

commit 347aa2f91bcdfdb772b631723db347f9a19d055c
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Thu Sep 24 16:07:53 2026 +0300

    Validate access token binding
---
 .../profile/impl/ValidateAccessTokenBinding.java   | 97 ++++++++++++++++++++++
 .../openid/vci/credentials/credentials-beans.xml   | 10 ++-
 .../openid/vci/credentials/credentials-flow.xml    |  1 +
 3 files changed, 105 insertions(+), 3 deletions(-)

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
new file mode 100644
index 0000000..d4f6b35
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ValidateAccessTokenBinding.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.geant.shibboleth.plugin.openidvci.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.AbstractOpenIDVCICredentialsValidationResponseAction;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultDPoPProofThumbprintLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+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.
+ *
+ * <p>Runs after DPoP proof validation, which is what makes the thumbprint of the proof key
+ * available.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link OidcEventIds#INVALID_ACCESS_TOKEN}
+ * @event {@link OidcEventIds#INVALID_DPOP_PROOF}
+ */
+public class ValidateAccessTokenBinding extends AbstractOpenIDVCICredentialsValidationResponseAction {
+
+    /** Class logger. */
+    @Nonnull
+    private Logger log = LoggerFactory.getLogger(ValidateAccessTokenBinding.class);
+
+    /** Strategy used to read the thumbprint of the validated DPoP proof. */
+    @Nonnull
+    private Function<ProfileRequestContext, String> dpopProofThumbprintLookupStrategy =
+            new DefaultDPoPProofThumbprintLookupFunction();
+
+    /**
+     * Set the strategy used to read the thumbprint of the validated DPoP proof.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setDpopProofThumbprintLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+
+        dpopProofThumbprintLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+        assert oidcResponseContext != null;
+        final TokenClaimsSet tokenClaims = oidcResponseContext.getAuthorizationGrantClaimsSet();
+        if (tokenClaims == null) {
+            log.error("{} No access token claims set to validate", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
+            return;
+        }
+
+        final String claimsSetThumbprint = tokenClaims.getDpopProofJwkThumbprint();
+        final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
+        if (claimsSetThumbprint != null || proofThumbprint != null) {
+            if (claimsSetThumbprint == null || proofThumbprint == null
+                    || !claimsSetThumbprint.equals(proofThumbprint)) {
+                log.warn("{} DPoP proof of client {} is not the key access token {} is bound to", getLogPrefix(),
+                        tokenClaims.getClientID(), tokenClaims.getID());
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+                return;
+            }
+        }
+
+        log.debug("{} Access token {} is bound to the key of the DPoP proof", getLogPrefix(), tokenClaims.getID());
+    }
+
+}
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 7b867fb..226ae40 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
@@ -32,6 +32,9 @@
         </property>
   </bean>
   
+  <bean id="ValidateAccessTokenBinding"
+        class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidateAccessTokenBinding" scope="prototype" />
+
   <bean id="ValidateRequestedCredential"
         class="org.geant.shibboleth.plugin.openidvci.profile.impl.ValidateRequestedCredential"
         p:credentialConfigurationsResolver-ref="openidvci.CredentialConfigurationsResolver" scope="prototype" />
@@ -138,7 +141,8 @@
   </bean>
     
   <bean id="ResolveCredentialLifetime" class="org.geant.shibboleth.plugin.openidvci.profile.impl.ResolveCredentialLifetime"
-        scope="prototype" />
+        scope="prototype"
+        p:timestampPrecision="%{openidvci.credentialTimestampPrecision:PT1H}" />
 
   <bean id="openidvci.IssuerLookupStrategy"
         class="org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction"
@@ -202,9 +206,9 @@
   </bean>
 
   <bean id="BuildErrorResponseFromEvent"
-        class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.BuildUserInfoErrorResponseFromEvent" scope="prototype"
+        class="org.geant.shibboleth.plugin.openidvci.profile.impl.BuildCredentialErrorResponseFromEvent" scope="prototype"
         p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier"
-        p:mappedErrors="#{getObject('shibboleth.oidc.userinfo.MappedErrors') ?: getObject('openidvci.credentials.MappedErrors')}"
+        p: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/credentials/credentials-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
index 46d71fd..ac86c21 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-flow.xml
@@ -28,6 +28,7 @@
   </action-state>
 
   <action-state id="ResumeAfterDoDPoPProofValidation">
+    <evaluate expression="ValidateAccessTokenBinding" />
     <evaluate expression="ParseProof" />
     <evaluate expression="PopulateProofSignatureValidationParameters" />
     <evaluate expression="ValidateProofSignature" />

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


More information about the commits mailing list