[java-idp-oidc] 01/03: Improve redirect URI validation in token request for unregistered clients
Codeberg
noreply at shibboleth.net
Thu Nov 5 17:42:12 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch maint-4.3
in repository java-idp-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-oidc/commit/3098ca59e4a9248ffa81f6e40d769a43257c65c3
commit 3098ca59e4a9248ffa81f6e40d769a43257c65c3
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 602f119c..5a14386a 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
@@ -93,6 +93,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 e2a48be1..d1a3e0eb 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
@@ -388,6 +388,28 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertNull(getSidFromJWT(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