[java-idp-oidc] 02/03: JOIDC-222 - Support for OpenID Federation

Henri Mikkonen henri.mikkonen at iki.fi
Fri May 2 12:02:00 UTC 2025


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

hjmikkon pushed a commit to branch dev/JOIDC-222
in repository java-idp-oidc.

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

commit f60166601fb9e7c9a2a14b82789eb549c7a821c3
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 2 11:16:51 2025 +0300

    JOIDC-222 - Support for OpenID Federation
    
    https://shibboleth.atlassian.net/browse/JOIDC-222
    
    Add federation-specific request object validation to authorize and PAR -flows
    - The federation draft mandates the use of jti and exp -claims and denies the use of sub -claim
      - Also the audience validation is stricter: no additional audiences must be specified
    - Default chain specified by flow beans shibboleth.oidfed(.par).DefaultSignedRequestObjectClaimsValidation
      - May be overridden via shibboleth.oidfed(.par).RequestObjectClaimsValidation
    - Covered by the flow tests
---
 .../oauth2/profile/impl/ValidateRequestObject.java | 46 +++++++++-
 .../pushed-authorization-beans.xml                 | 34 +++++++-
 .../idp/flows/oidc/authorize/authorize-beans.xml   | 32 ++++++-
 .../profile/impl/ValidateRequestObjectTest.java    | 27 ++++++
 .../AuthorizeFlowAutomaticRegistrationTest.java    | 74 +++++++++++++++++
 ...shedAuthorizeFlowAutomaticRegistrationTest.java | 97 ++++++++++++++++++++++
 6 files changed, 306 insertions(+), 4 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java
index 874eac2b..3f0ddec4 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObject.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
 import java.text.ParseException;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -62,6 +63,12 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
     /** The claims validator to be applied for validating the plain/unsigned request object. */
     @NonnullAfterInit private ClaimsValidator plainClaimsValidator;
 
+    /** The claims validator to be applied for validating the federation request object. */
+    @NonnullAfterInit private ClaimsValidator federationClaimsValidator;
+
+    /** The predicate used for using federation claims validator instead of signed claims validator. */
+    @NonnullAfterInit private Predicate<ProfileRequestContext> useFederationClaimsValidatorCondition;
+
     /** The flag to control whether to use the PAR-endpoint logic for the validation. */
     private boolean parEndpointLogic;
 
@@ -80,6 +87,18 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
         signedClaimsValidator = Constraint.isNotNull(validator, "Signed claims validator cannot be null");
     }
 
+    /**
+     * Set the claims validator used for validating the federation request object.
+     * 
+     * @param validator What to set
+     * 
+     * @since 4.3.0
+     */
+    public void setFederationClaimsValidator(@Nonnull final ClaimsValidator validator) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        federationClaimsValidator = Constraint.isNotNull(validator, "Federation claims validator cannot be null");
+    }
+
     /**
      * Set the claims validator used for validating the plain/unsigned request object.
      * 
@@ -90,6 +109,17 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
         plainClaimsValidator = Constraint.isNotNull(validator, "Plain claims validator cannot be null");
     }
 
+    /**
+     * Set the predicate used for using federation claims validator instead of signed claims validator.
+     * 
+     * @param condition What to set
+     * 
+     * @since 4.3.0
+     */
+    public void setUseFederationClaimsValidatorCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        useFederationClaimsValidatorCondition =
+                Constraint.isNotNull(condition, "Use federation claims validator condition cannot be null");
+    }
     /**
      * Set the flag to control whether to use the PAR-endpoint logic for the validation.
      * 
@@ -109,10 +139,18 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
             throw new ComponentInitializationException("ClaimsValidator for signed requests cannot be null");
         }
 
+        if (federationClaimsValidator == null) {
+            throw new ComponentInitializationException("ClaimsValidator for federation requests cannot be null");
+        }
+
         if (plainClaimsValidator == null) {
             throw new ComponentInitializationException("ClaimsValidator for plain requests cannot be null");
         }
-}
+
+        if (useFederationClaimsValidatorCondition == null) {
+            throw new ComponentInitializationException("UseFederationClaimsValidatorCondition cannot be null");
+        }
+    }
 
     /** {@inheritDoc} */
     @Override
@@ -224,7 +262,11 @@ public class ValidateRequestObject extends AbstractOAuthAuthorizationResponseAct
         try {
             if (requestObject instanceof SignedJWT) {
                 // Signature of signed request object is handled earlier in the flow
-                signedClaimsValidator.validate(claimsSet, profileRequestContext);
+                if (useFederationClaimsValidatorCondition.test(profileRequestContext)) {
+                    federationClaimsValidator.validate(claimsSet, profileRequestContext);
+                } else {
+                    signedClaimsValidator.validate(claimsSet, profileRequestContext);
+                }
             } else {
                 plainClaimsValidator.validate(claimsSet, profileRequestContext);
             }
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 1707e1ef..8b499cd4 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
@@ -230,7 +230,13 @@
         scope="prototype"
         p:plainClaimsValidator="#{getObject('shibboleth.oidc.par.DefaultPlainRequestObjectClaimsValidation')}"
         p:signedClaimsValidator="#{getObject('shibboleth.oidc.par.SignedRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.par.DefaultSignedRequestObjectClaimsValidation')}"
-        p:parEndpointLogic="true"/>
+        p:parEndpointLogic="true"
+        p:federationClaimsValidator="#{getObject('shibboleth.oidfed.par.RequestObjectClaimsValidation') ?: getObject('shibboleth.oidfed.par.DefaultRequestObjectClaimsValidation')}">
+        <property name="useFederationClaimsValidatorCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+        </property>
+    </bean>
 
     <bean id="shibboleth.oidc.par.DefaultPlainRequestObjectClaimsValidation"
         class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator">
@@ -243,6 +249,10 @@
         class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
         p:claimValidators-ref="SignedClaimsValidators" />
 
+    <bean id="shibboleth.oidfed.par.DefaultRequestObjectClaimsValidation"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="FederationClaimsValidators" />
+
     <bean id="ExpiryClaimsValidator"
         class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
         p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
@@ -288,6 +298,28 @@
         <ref bean="AudienceClaimsValidator" />
     </util:list>
 
+    <util:list id="FederationClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+        <ref bean="IssuerClaimsValidator" />
+        <ref bean="ClientIdClaimsValidator" />
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator"
+            p:extraAudienceValidation="true">
+            <property name="audienceLookupStrategy">
+                <bean parent="shibboleth.BiFunctions.Expression"
+                    c:expression="#custom.apply(#input1)"
+                    p:customObject-ref="shibboleth.ResponderIdLookup.Simple" />
+            </property>
+        </bean>
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
+            p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+            p:replayCache-ref="shibboleth.ReplayCache" />
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+            p:requiredClaims="exp" />
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+            p:prohibitedClaims="sub" />
+    </util:list>
+
     <bean id="DefaultParUseOnlyRequestObjectCondition" parent="shibboleth.BiConditions.Expression"
         c:expression="#input1 != null and #input1.getFirst() != null and #input1.getFirst().specifiesRequestObject()" />
 
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 9a09cce2..5acb6b53 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
@@ -390,7 +390,12 @@
     <bean id="ValidateRequestObject" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateRequestObject"
         scope="prototype"
         p:plainClaimsValidator="#{getObject('shibboleth.oidc.PlainRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation')}"
-        p:signedClaimsValidator="#{getObject('shibboleth.oidc.SignedRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation')}">
+        p:signedClaimsValidator="#{getObject('shibboleth.oidc.SignedRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation')}"
+        p:federationClaimsValidator="#{getObject('shibboleth.oidfed.RequestObjectClaimsValidation') ?: getObject('shibboleth.oidfed.DefaultRequestObjectClaimsValidation')}">
+        <property name="useFederationClaimsValidatorCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                c:expression="#input.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oidfed.profile.impl.RelyingPartyTrustChainContext))" />
+        </property>
     </bean>
 
     <bean id="shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation"
@@ -401,6 +406,10 @@
         class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
         p:claimValidators-ref="SignedClaimsValidators" />
 
+    <bean id="shibboleth.oidfed.DefaultRequestObjectClaimsValidation"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="FederationClaimsValidators" />
+
     <bean id="ExpiryClaimsValidator"
         class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
         p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
@@ -440,6 +449,27 @@
         <ref bean="AudienceClaimsValidator" />
     </util:list>
 
+    <util:list id="FederationClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+        <ref bean="IssuerClaimsValidator" />
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator"
+            p:extraAudienceValidation="true">
+            <property name="audienceLookupStrategy">
+                <bean parent="shibboleth.BiFunctions.Expression"
+                    c:expression="#custom.apply(#input1)"
+                    p:customObject-ref="shibboleth.ResponderIdLookup.Simple" />
+            </property>
+        </bean>
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
+            p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+            p:replayCache-ref="shibboleth.ReplayCache" />
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator"
+            p:requiredClaims="exp" />
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
+            p:prohibitedClaims="sub" />
+    </util:list>
+
     <bean id="DefaultRequestRedirectURILookupFunction"
         class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultRequestRedirectURILookupFunction"
         p:useOnlyRequestObjectPredicate-ref="#{'%{idp.oauth2.authorize.useOnlyRequestObject:DefaultUseOnlyRequestObjectCondition}'.trim()}"/>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObjectTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObjectTest.java
index f7b80f58..354b6aba 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObjectTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRequestObjectTest.java
@@ -26,6 +26,7 @@ import net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator;
 import net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator;
 import net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator;
 import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.PredicateSupport;
 
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -98,6 +99,8 @@ public class ValidateRequestObjectTest {
         action = new ValidateRequestObject();
         action.setPlainClaimsValidator(buildPlainClaimsValidation());
         action.setSignedClaimsValidator(buildSignedClaimsValidation("000123"));
+        action.setFederationClaimsValidator(buildSignedClaimsValidation("000123"));
+        action.setUseFederationClaimsValidatorCondition(PredicateSupport.alwaysFalse());
         action.initialize();
     }
     
@@ -134,6 +137,8 @@ public class ValidateRequestObjectTest {
     public void testInitFailsNoPlainValidator() throws ComponentInitializationException {
         action = new ValidateRequestObject();
         action.setSignedClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setFederationClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setUseFederationClaimsValidatorCondition(PredicateSupport.alwaysFalse());
         action.initialize();
     }
 
@@ -141,6 +146,26 @@ public class ValidateRequestObjectTest {
     public void testInitFailsNoSignedValidators() throws ComponentInitializationException {
         action = new ValidateRequestObject();
         action.setPlainClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setFederationClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setUseFederationClaimsValidatorCondition(PredicateSupport.alwaysFalse());
+        action.initialize();
+    }
+
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void testInitFailsNoFederationValidators() throws ComponentInitializationException {
+        action = new ValidateRequestObject();
+        action.setPlainClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setSignedClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setUseFederationClaimsValidatorCondition(PredicateSupport.alwaysFalse());
+        action.initialize();
+    }
+
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void testInitFailsNoUseFederationClaimsCondition() throws ComponentInitializationException {
+        action = new ValidateRequestObject();
+        action.setPlainClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setSignedClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setFederationClaimsValidator(new ChainingJWTClaimsValidator());
         action.initialize();
     }
 
@@ -149,6 +174,8 @@ public class ValidateRequestObjectTest {
         action = new ValidateRequestObject();
         action.setPlainClaimsValidator(new ChainingJWTClaimsValidator());
         action.setSignedClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setFederationClaimsValidator(new ChainingJWTClaimsValidator());
+        action.setUseFederationClaimsValidatorCondition(PredicateSupport.alwaysFalse());
         action.initialize();
     }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 4cdb852f..592d071c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -20,9 +20,12 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.webflow.executor.FlowExecutionResult;
@@ -66,6 +69,8 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
                 launchAuthenticationRequest(clientId, "openid profile", plainRequestObject(Map.of(
                         "iss", clientId,
                         "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
                         "response_type", "code",
                         "scope", "openid profile",
                         "redirect_uri", redirectUri)));
@@ -81,6 +86,8 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
                 launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
                         "iss", clientId,
                         "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
                         "response_type", "code",
                         "scope", "openid profile",
                         "redirect_uri", redirectUri)));
@@ -93,6 +100,73 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
     }
 
+    @Test
+    public void testWithValidTrustChain_signedRequestObject_missingJti()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        assertErrorCode(result, "invalid_request_object");
+    }
+
+    @Test
+    public void testWithValidTrustChain_signedRequestObject_missingExp()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "aud", issuer,
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        assertErrorCode(result, "invalid_request_object");
+    }
+
+    @Test
+    public void testWithValidTrustChain_signedRequestObject_containsForbiddenSub()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "sub", "mockValue",
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        assertErrorCode(result, "invalid_request_object");
+    }
+
+    @Test
+    public void testWithValidTrustChain_signedRequestObject_containsAdditionalAudience()
+            throws IOException, UnsupportedOperationException, URISyntaxException {
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId);
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "aud", List.of(issuer, "https://another.example.org"),
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri)));
+        assertErrorCode(result, "invalid_request_object");
+    }
+
     @Test
     public void testWithValidTrustChain_leafKeySignedRequestObject()
             throws IOException, UnsupportedOperationException, URISyntaxException {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
index b84a33f8..ddf5dd40 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
@@ -19,12 +19,14 @@ import java.net.URI;
 import java.net.URLEncoder;
 import java.security.PublicKey;
 import java.text.ParseException;
+import java.time.Duration;
 import java.time.Instant;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.webflow.executor.FlowExecutionResult;
@@ -95,6 +97,8 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
                 "client_id", clientId,
                 "iss", clientId,
                 "aud", issuer,
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                "jti", UUID.randomUUID(),
                 "response_type", "code",
                 "scope", "openid",
                 "redirect_uri", redirectUri)).serialize()));
@@ -106,6 +110,97 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
     }
 
+    @Test
+    public void testWithPublicClientWithSignedRequestObject_missingJti() throws Exception {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setResponseTypes(Set.of(ResponseType.CODE));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId, metadata);
+        setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+                "client_id", clientId,
+                "iss", clientId,
+                "aud", issuer,
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                "response_type", "code",
+                "scope", "openid",
+                "redirect_uri", redirectUri)).serialize()));
+        final FlowExecutionResult result =
+                flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+
+    @Test
+    public void testWithPublicClientWithSignedRequestObject_missingExp() throws Exception {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setResponseTypes(Set.of(ResponseType.CODE));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId, metadata);
+        setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+                "client_id", clientId,
+                "iss", clientId,
+                "aud", issuer,
+                "jti", UUID.randomUUID(),
+                "response_type", "code",
+                "scope", "openid",
+                "redirect_uri", redirectUri)).serialize()));
+        final FlowExecutionResult result =
+                flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+
+    @Test
+    public void testWithPublicClientWithSignedRequestObject_constainsForbiddenSub() throws Exception {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setResponseTypes(Set.of(ResponseType.CODE));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId, metadata);
+        setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+                "client_id", clientId,
+                "sub", "mockValue",
+                "iss", clientId,
+                "aud", issuer,
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                "jti", UUID.randomUUID(),
+                "response_type", "code",
+                "scope", "openid",
+                "redirect_uri", redirectUri)).serialize()));
+        final FlowExecutionResult result =
+                flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+
+    @Test
+    public void testWithPublicClientWithSignedRequestObject_containsAdditionalAudience() throws Exception {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setResponseTypes(Set.of(ResponseType.CODE));
+        metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+        metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+        final String clientId = uniqueClientId();
+        configureMockHttpClient(clientId, metadata);
+        setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+                "client_id", clientId,
+                "iss", clientId,
+                "aud", List.of(issuer, "https://additional.example.org"),
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                "jti", UUID.randomUUID(),
+                "response_type", "code",
+                "scope", "openid",
+                "redirect_uri", redirectUri)).serialize()));
+        final FlowExecutionResult result =
+                flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+
     @Test
     public void testWithPublicClientWithPlainRequestObject() throws Exception {
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
@@ -119,6 +214,8 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
                 "client_id", clientId,
                 "iss", clientId,
                 "aud", issuer,
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+                "jti", UUID.randomUUID(),
                 "response_type", "code",
                 "scope", "openid",
                 "redirect_uri", redirectUri)).serialize()));

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


More information about the commits mailing list