[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