[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