[java-idp-oidc] branch main updated: JOIDC-253 - Facilitate extending the default set of request object claim validators

Henri Mikkonen henri.mikkonen at iki.fi
Fri Sep 5 12:54:58 UTC 2025


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

hjmikkon pushed a commit to branch main
in repository java-idp-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=57d367d087d8c87238034f7d5ec7a0a545c3afad

The following commit(s) were added to refs/heads/main by this push:
     new 57d367d0 JOIDC-253 - Facilitate extending the default set of request object claim validators
57d367d0 is described below

commit 57d367d087d8c87238034f7d5ec7a0a545c3afad
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 5 15:54:41 2025 +0300

    JOIDC-253 - Facilitate extending the default set of request object claim validators
    
    https://shibboleth.atlassian.net/browse/JOIDC-253
    
    - The new AutowiringRequestObjectClaimsValidator auto-wires any free RequestObjectClaimsValidator beans
      - It's used in the default DefaultSignedRequestObjectClaimsValidation set for both authorize and PAR endpoints
      - The base-list of validators are set in the same way as for ChainingJWTClaimsValidator
---
 .../jwt/claims/RequestObjectClaimsValidator.java   |  56 ++++++++++
 .../oidc/op/security/jwt/claims/package-info.java  |  16 +++
 .../AutowiringRequestObjectClaimsValidator.java    | 117 +++++++++++++++++++++
 .../pushed-authorization-beans.xml                 |   2 +-
 .../idp/flows/oidc/authorize/authorize-beans.xml   |   2 +-
 .../oidc/op/profile/flow/AuthorizeFlowTest.java    |  59 +++++++++++
 .../flow/TestRequestObjectClaimsValidator.java     |  59 +++++++++++
 .../net/shibboleth/idp/module/conf/global.xml      |   7 ++
 8 files changed, 316 insertions(+), 2 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/RequestObjectClaimsValidator.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/RequestObjectClaimsValidator.java
new file mode 100644
index 00000000..437b3156
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/RequestObjectClaimsValidator.java
@@ -0,0 +1,56 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.security.jwt.claims;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A wrapper class for {@link ClaimsValidator} that is meant for validating request object claims.
+ * 
+ * @since 4.4.0
+ */
+public class RequestObjectClaimsValidator extends AbstractIdentifiableInitializableComponent
+    implements ClaimsValidator {
+
+    /** The embedded {@link ClaimsValidator}. */
+    @Nonnull private final ClaimsValidator claimsValidator;
+
+    /**
+     * Constructor.
+     *
+     * @param validator the embedded validator
+     */
+    public RequestObjectClaimsValidator(@Nonnull final ClaimsValidator validator) {
+        claimsValidator = Constraint.isNotNull(validator, "ClaimsValidator cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void validate(@Nullable final JWTClaimsSet claims, @Nonnull final ProfileRequestContext context)
+            throws JWTValidationException {
+        claimsValidator.validate(claims, context);
+    }
+
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/package-info.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/package-info.java
new file mode 100644
index 00000000..610884d9
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/package-info.java
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+/** Validation functions for JWT claims. */
+package net.shibboleth.idp.plugin.oidc.op.security.jwt.claims;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/AutowiringRequestObjectClaimsValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/AutowiringRequestObjectClaimsValidator.java
new file mode 100644
index 00000000..d48a214e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/AutowiringRequestObjectClaimsValidator.java
@@ -0,0 +1,117 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.RequestObjectClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A container for {@link ClaimsValidator}s for request object validation. All the free-standing validators of type
+ * {@link RequestObjectClaimsValidator} are auto-wired.
+ * 
+ * @since 4.4.0
+ */
+public class AutowiringRequestObjectClaimsValidator extends AbstractIdentifiableInitializableComponent
+    implements ClaimsValidator {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AutowiringRequestObjectClaimsValidator.class);
+
+    /** Initial list of claim validators. */
+    @Nonnull @NonnullElements private List<ClaimsValidator> initialClaimValidators;
+
+    /** List of claim validators. */
+    @Nonnull @NonnullElements private List<ClaimsValidator> claimValidators;
+
+    /**
+     * Auto-wiring point for free-standing objects.
+     * 
+     * @param freeObjects free-standing objects
+     */
+    @Autowired
+    public AutowiringRequestObjectClaimsValidator(@Nullable Collection<RequestObjectClaimsValidator> freeObjects) {
+        if (freeObjects != null) {
+            initialClaimValidators = CollectionSupport.copyToList(freeObjects);
+        } else {
+            initialClaimValidators = CollectionSupport.emptyList();
+        }
+        claimValidators = initialClaimValidators;
+    }
+
+    /**
+     * Set the list of non-autowired validators to use.
+     * 
+     * @param validators validators to use
+     */
+    public void setClaimValidators(@Nullable @NonnullElements final List<ClaimsValidator> validators) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        if (validators != null) {
+            final Collection<ClaimsValidator> holder = new LinkedHashSet<>(validators);
+            holder.addAll(
+                    initialClaimValidators.stream()
+                        .filter(obj -> {
+                            if (holder.contains(obj)) {
+                                log.info("Replacing auto-wired component: {}", obj.getId());
+                                return false;
+                            }
+                            return true;
+                        })
+                        .collect(Collectors.toUnmodifiableList()));
+            claimValidators = CollectionSupport.copyToList(holder);
+        } else {
+            claimValidators = initialClaimValidators;
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void validate(@Nullable final JWTClaimsSet claims, @Nonnull final ProfileRequestContext context) 
+        throws JWTValidationException {       
+
+        if (claimValidators.isEmpty()) {
+            log.trace("{}: No validators to check, nothing to do", getId());
+            return;
+        }
+
+        log.debug("{}: Attempting JWT claims validation for subject '{}'", getId(), claims.getSubject());
+
+        for (final ClaimsValidator validator : claimValidators) {
+            log.trace("{}: Attempting JWT claims validation '{}'", getId(), validator.getId());
+            validator.validate(claims, context);
+        }
+
+        log.debug("{}: JWT claims validation for subject '{}' succeeded", getId(), claims.getSubject());
+    }   
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
index 91721d52..702ca83c 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
@@ -219,7 +219,7 @@
     </bean>
 
     <bean id="shibboleth.oidc.par.DefaultSignedRequestObjectClaimsValidation"
-        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.AutowiringRequestObjectClaimsValidator"
         p:claimValidators-ref="SignedClaimsValidators" />
 
     <bean id="ExpiryClaimsValidator"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index af8c7aa6..43ed51da 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -378,7 +378,7 @@
         p:claimValidators-ref="PlainClaimsValidators" />
 
     <bean id="shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation"
-        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.AutowiringRequestObjectClaimsValidator"
         p:claimValidators-ref="SignedClaimsValidators" />
 
     <bean id="ExpiryClaimsValidator"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index 9aaa7876..8eb034b1 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -2356,6 +2356,65 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         Assert.assertNull(successResponse.getIssuer());
     }
 
+    @Test
+    public void testWithHS256SignedReqObjectExtraValidatorFailing() throws IOException, ParseException,
+            SessionException, JOSEException {
+        final String clientId = "clientIdRequireExtraRequestObjectClaim";
+        final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+                .audience(issuer)
+                .issuer(clientId)
+                .claim("redirect_uri", redirectUri)
+                .build();
+        final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("request", requestObject.serialize())));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        if (!result.getOutcome().getId().equals("ErrorView")) {
+            assertErrorCode(result, "invalid_request_object");
+        }
+    }
+
+    @Test
+    public void testWithHS256SignedReqObjectExtraValidatorSuccess() throws IOException, ParseException,
+            SessionException, JOSEException {
+        final String clientId = "clientIdRequireExtraRequestObjectClaim";
+        final JWTClaimsSet ro = new JWTClaimsSet.Builder()
+                .audience(issuer)
+                .issuer(clientId)
+                .claim("redirect_uri", redirectUri)
+                .claim("enforcedClaim", "enforcedValue")
+                .build();
+        final SignedJWT requestObject = createSecretJWT(ro, clientSecret);
+        request.setMethod("GET");
+        setRequestParameters(List.of(new Pair<>("client_id", clientId),
+                new Pair<>("response_type", "code"),
+                new Pair<>("scope", "openid profile"),
+                new Pair<>("redirect_uri", redirectUri),
+                new Pair<>("request", requestObject.serialize())));
+        storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+        initializeThreadLocals();
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        Assert.assertNotNull(successResponse.getAuthorizationCode());
+        Assert.assertNotNull(getSidFromAuthorizeCodeClaimsSet(successResponse));
+        Assert.assertNull(successResponse.getIssuer());
+    }
+
     @Factory
     public Object[] createIdTokenSecurityTests() {
         return new Object[] {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRequestObjectClaimsValidator.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRequestObjectClaimsValidator.java
new file mode 100644
index 00000000..d01a3869
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TestRequestObjectClaimsValidator.java
@@ -0,0 +1,59 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+
+/**
+ * Test request object claims validator that activates solely for client 'clientIdRequireExtraRequestObjectClaim' in the
+ * authorization request.
+ */
+public class TestRequestObjectClaimsValidator extends AbstractClaimsValidator {
+
+    public TestRequestObjectClaimsValidator() {
+        setActivationCondition((prc, claims) -> {
+            return Optional.ofNullable(prc)
+                    .map(ctx -> ctx.ensureInboundMessageContext().getMessage())
+                    .filter(AuthorizationRequest.class::isInstance)
+                    .map(AuthorizationRequest.class::cast)
+                    .map(AuthorizationRequest::getClientID)
+                    .map(ClientID::getValue)
+                    .map(id -> "clientIdRequireExtraRequestObjectClaim".equals(id))
+                    .orElse(false);
+                    
+        });
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doValidate(@Nonnull final JWTClaimsSet claims, @Nonnull final ProfileRequestContext context)
+            throws JWTValidationException {
+        if (!"enforcedValue".equals(claims.getClaim("enforcedClaim"))) {
+            throw new JWTValidationException("enforcedClaim was not equal to enforcedValue");
+        }
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
index 23ee01ca..0614c937 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
@@ -116,4 +116,11 @@
     <bean p:id="oidc/metadata-lookup-ext/lookupext1" parent="shibboleth.oidc.MetadataLookupExtensionFlow" />
     <bean p:id="oidc/metadata-lookup-ext/lookupext2" parent="shibboleth.oidc.MetadataLookupExtensionFlow" />
 
+    <bean class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.RequestObjectClaimsValidator">
+        <constructor-arg>
+            <bean id="testRequestObjectClaimsValidation"
+                class="net.shibboleth.idp.plugin.oidc.op.profile.flow.TestRequestObjectClaimsValidator"/>
+        </constructor-arg>
+    </bean>
+
 </beans>

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


More information about the commits mailing list