[java-idp-oidc] 01/03: Improve redirect URI validation in token request for unregistered clients

Codeberg noreply at shibboleth.net
Thu Nov 5 17:34:37 UTC 2026


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

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

View the commit online:
https://codeberg.org/Shibboleth/java-idp-oidc/commit/067f01cede466975b50037c2d289fe3bd460be94

commit 067f01cede466975b50037c2d289fe3bd460be94
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Nov 5 18:21:04 2026 +0200

    Improve redirect URI validation in token request for unregistered clients
    
    https://shibboleth.atlassian.net/browse/JOIDC-299
    
    - Refactored ValidateRedirectURI action to have a flag for requiring the match for unregistered clients
      - Defaults to 'false', configured to 'true' in the token flow
    - If set to true, require check against the valid redirect URIs (fetched via authorization grant in the token flow)
    - Cover in flow tests
---
 .../oauth2/profile/impl/ValidateRedirectURI.java   | 32 ++++++++++++++++++----
 .../idp/flows/oidc/token/token-beans.xml           |  1 +
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java | 22 +++++++++++++++
 3 files changed, 50 insertions(+), 5 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java
index 78be1431..17b43b21 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java
@@ -69,6 +69,9 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
     /** Whether to require redirect uri value in the request also when only single value is registered. */
     private boolean requireRequestedValue = true;
 
+    /** Whether to require match with {@link #validRedirectURIsLookupStrategy} also for unregistered clients. */
+    private boolean requireUnregisteredMatch = false;
+
     /** Strategy to obtain custom redirect URI validation strategy. */
     @Nonnull private Function<ProfileRequestContext,BiPredicate<URI, ProfileRequestContext>>
         customRedirectUriValidationStrategyLookupStrategy;
@@ -89,7 +92,7 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
      * @param strategy lookup strategy
      */
     public void setRedirectURILookupStrategy(@Nonnull final Function<ProfileRequestContext, URI> strategy) {
-        ifInitializedThrowUnmodifiabledComponentException();
+        checkSetterPreconditions();
         redirectURILookupStrategy =
                 Constraint.isNotNull(strategy, "RedirectURILookupStrategy lookup strategy cannot be null");
     }
@@ -100,7 +103,7 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
      * @param strategy lookup strategy
      */
     public void setValidRedirectURIsLookupStrategy(@Nonnull final Function<ProfileRequestContext, Set<URI>> strategy) {
-        ifInitializedThrowUnmodifiabledComponentException();
+        checkSetterPreconditions();
         validRedirectURIsLookupStrategy =
                 Constraint.isNotNull(strategy, "ValidRedirectURIsLookupStrategy lookup strategy cannot be null");
     }
@@ -112,7 +115,7 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
      */
     public void setRegisteredRedirectURIsLookupStrategy(@Nonnull final Function<ProfileRequestContext,
             Set<URI>> strategy) {
-        ifInitializedThrowUnmodifiabledComponentException();
+        checkSetterPreconditions();
         registeredRedirectURIsLookupStrategy =
                 Constraint.isNotNull(strategy, "RegisteredRedirectURIsLookupStrategy lookup strategy cannot be null");
     }
@@ -123,7 +126,7 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
      * @param flag flag to set
      */
     public void setRequireRequestedValue(final boolean flag) {
-        ifInitializedThrowUnmodifiabledComponentException();
+        checkSetterPreconditions();
         requireRequestedValue = flag;
     }
 
@@ -168,6 +171,16 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
                 Constraint.isNotNull(strategy, "Custom redirect URI validation lookup strategy cannot be null");
     }
 
+    /**
+     * Set whether to require match with {@link #validRedirectURIsLookupStrategy} also for unregistered clients.
+     * 
+     * @param flag flag to set
+     */
+    public void setRequireUnregisteredMatch(final boolean flag) {
+        checkSetterPreconditions();
+        requireUnregisteredMatch = flag;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -188,6 +201,8 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
         final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
         assert oidcResponseContext != null;
 
+        final Set<URI> redirectionURIs = validRedirectURIsLookupStrategy.apply(profileRequestContext);
+
         final BiPredicate<URI, ProfileRequestContext> customRedirectUriValidationStrategy =
                 customRedirectUriValidationStrategyLookupStrategy.apply(profileRequestContext);
         if (customRedirectUriValidationStrategy != null) {
@@ -212,6 +227,14 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
                     final Boolean enforcerResult = result.getSecond();
                     if (enforcerResult != null && enforcerResult.booleanValue() 
                             && requestRedirectURI.toString().equals(result.getFirst())) {
+                        if (requireUnregisteredMatch &&
+                                (redirectionURIs == null || !redirectionURIs.contains(requestRedirectURI))) {
+                            log.warn("{} Policy-accepted URI {} does not match with the valid URIs {}", getLogPrefix(),
+                                    requestRedirectURI, redirectionURIs);
+                            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
+                            return;
+                        }
+
                         log.debug("{} Redirection URI {} accepted by the policy for unregistered clients",
                                 getLogPrefix(), requestRedirectURI);
                         oidcResponseContext.setRedirectURI(requestRedirectURI);
@@ -225,7 +248,6 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
             return;
         }
 
-        final Set<URI> redirectionURIs = validRedirectURIsLookupStrategy.apply(profileRequestContext);
         if (redirectionURIs == null || redirectionURIs.isEmpty()) {
             log.warn("{} Client has not registered Redirection URIs. Redirection URI cannot be validated.",
                     getLogPrefix());
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
index ef14c177..8c6e0cad 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
@@ -95,6 +95,7 @@
         p:redirectURILookupStrategy-ref="shibboleth.TokenRequestRedirectURILookupStrategy"
         p:validRedirectURIsLookupStrategy-ref="shibboleth.TokenRequestValidRequestUrisLookupStrategy"
         p:requireRequestedValue="false"
+        p:requireUnregisteredMatch="true"
         p:unregisteredClientPolicyEnforcer="#{getObject('shibboleth.oidc.UnregisteredClientPolicyEnforcer') ?: getObject('shibboleth.oidc.DefaultUnregisteredClientPolicyEnforcer')}"/>
 
     <bean id="shibboleth.TokenRequestRedirectURILookupStrategy"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 70a43bfd..5e3291ae 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -404,6 +404,28 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         assertDecoratedIdToken(response.getOIDCTokens().getIDToken());
     }
 
+    @Test
+    public void testValidGrant_unregisterdClient_policyCompliant_unmatchingCodeRedirectUri() throws Exception {
+        final String clientId = "policyAcceptedClient1";
+        final String authorizationCode = new AuthorizeCodeClaimsSet.Builder()
+                .setJWTID(new SecureRandomIdentifierGenerationStrategy())
+                .setClientID(new ClientID(clientId))
+                .setIssuer("https://op.example.org")
+                .setPrincipal("jdoe")
+                .setSubject("mock")
+                .setIssuedAt(Instant.now())
+                .setExpiresAt(Instant.now().plusSeconds(100))
+                .setAuthenticationTime(Instant.now())
+                .setRedirectURI(new URI(redirectUri + "wrong"))
+                .setScope(scope).build().serialize(getDataSealer());
+
+        setHttpFormRequest("POST", createRequestParameters(redirectUri, "authorization_code",
+                authorizationCode, clientId));
+        setBasicAuth(clientId, clientSecret);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+    }
+
     @Test
     public void testValidGrant_unregisterdClient_policyNonCompliantClientID() throws Exception {
         final String clientId = "mockClientId";

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


More information about the commits mailing list