[java-idp-oidc] branch main updated: JOIDC-259 - Option to ignore an invalid post_logout_redirect_uri instead of fail

Codeberg noreply at shibboleth.net
Thu Jun 25 08:09:39 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/f80ebec709e4b36a325d44f2ab9c6a7c790e559b

The following commit(s) were added to refs/heads/main by this push:
     new f80ebec7 JOIDC-259 - Option to ignore an invalid post_logout_redirect_uri instead of fail
f80ebec7 is described below

commit f80ebec709e4b36a325d44f2ab9c6a7c790e559b
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Jun 25 11:08:50 2026 +0300

    JOIDC-259 - Option to ignore an invalid post_logout_redirect_uri instead of fail
    
    https://shibboleth.atlassian.net/browse/JOIDC-259
    
    Exploit the new OIDC.Logout option 'ignoreInvalidPostLogoutRedirectUri'
    - Also wire 'idp.oidc.logout.ignoreInvalidPostLogoutRedirectUri' to OIDC.Logout and OIDC.Logout.MDDriven, defaults to 'false'
---
 .../impl/ValidatePostLogoutRedirectURI.java        | 55 +++++++++++++++++++---
 .../idp/service/relying-party/postconfig.xml       | 11 ++++-
 .../idp/plugin/oidc/op/conf/oidc.properties        |  3 ++
 .../impl/ValidatePostLogoutRedirectURITest.java    | 45 ++++++++++++++++--
 .../oidc/op/profile/flow/EndSessionFlowTest.java   | 42 ++++++++++++++++-
 .../shibboleth/idp/module/conf/relying-party.xml   |  7 +++
 6 files changed, 150 insertions(+), 13 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURI.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURI.java
index a522f5f5..4b25060f 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURI.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURI.java
@@ -17,6 +17,7 @@ package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
 import java.net.URI;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -27,6 +28,7 @@ import org.slf4j.Logger;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPostLogoutRedirectURIValidationPredicate;
+import net.shibboleth.oidc.profile.config.logic.IgnoreInvalidPostLogoutRedirectUriPredicate;
 import net.shibboleth.oidc.profile.config.navigate.CustomPostLogoutRedirectUriValidationStrategyLookupFunction;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.shared.logic.Constraint;
@@ -47,6 +49,9 @@ public class ValidatePostLogoutRedirectURI extends AbstractOIDCRpInitiatedLogout
     @Nonnull private Function<ProfileRequestContext,BiPredicate<URI, ProfileRequestContext>>
         customPostLogoutRedirectUriValidationStrategyLookupStrategy;
 
+    /** Condition whether to ignore invalid post logout redirect URI. */
+    @Nonnull private Predicate<ProfileRequestContext> ignoreInvalidPostLogoutRedirectUriPredicate;
+
     /** The redirect URI to be validated. */
     @Nullable private URI requestedRedirectURI;
 
@@ -57,6 +62,7 @@ public class ValidatePostLogoutRedirectURI extends AbstractOIDCRpInitiatedLogout
         redirectURIValidationStrategy = new DefaultPostLogoutRedirectURIValidationPredicate();
         customPostLogoutRedirectUriValidationStrategyLookupStrategy =
                 new CustomPostLogoutRedirectUriValidationStrategyLookupFunction();
+        ignoreInvalidPostLogoutRedirectUriPredicate = new IgnoreInvalidPostLogoutRedirectUriPredicate();
     }
 
     /**
@@ -86,6 +92,19 @@ public class ValidatePostLogoutRedirectURI extends AbstractOIDCRpInitiatedLogout
                 "Custom post logout redirect URI validation lookup strategy cannot be null");
     }
 
+    /**
+     * Set the condition whether to ignore invalid post logout redirect URI.
+     * 
+     * @param condition predicate
+     */
+    public void setIgnoreInvalidPostLogoutRedirectUriPredicate(@Nonnull
+            final Predicate<ProfileRequestContext> condition) {
+        checkSetterPreconditions();
+
+        ignoreInvalidPostLogoutRedirectUriPredicate = Constraint.isNotNull(condition,
+                "Ignore invalid post logout redirection URI predicate cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -108,13 +127,21 @@ public class ValidatePostLogoutRedirectURI extends AbstractOIDCRpInitiatedLogout
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final boolean ignoreInvalidPostLogoutRedirectUri =
+                ignoreInvalidPostLogoutRedirectUriPredicate.test(profileRequestContext);
         final BiPredicate<URI, ProfileRequestContext> customPostLogoutRedirectUriValidationStrategy =
                 customPostLogoutRedirectUriValidationStrategyLookupStrategy.apply(profileRequestContext);
         if (customPostLogoutRedirectUriValidationStrategy != null) {
             if (!customPostLogoutRedirectUriValidationStrategy.test(requestedRedirectURI, profileRequestContext)) {
-                log.warn("{} Custom post logout redirect URI validation failed for {}", getLogPrefix(),
-                        requestedRedirectURI);
-                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
+                if (!ignoreInvalidPostLogoutRedirectUri) {
+                    log.warn("{} Custom post logout redirect URI validation failed for {}", getLogPrefix(),
+                            requestedRedirectURI);
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
+                } else {
+                    log.info("{} Ignoring failed custom redirect URI validation for {}", getLogPrefix(),
+                            requestedRedirectURI);
+                    removePostLogoutRedirectUri();
+                }
             } else {
                 log.debug("{} Custom post logout redirect URI validation successful for {}", getLogPrefix(),
                         requestedRedirectURI);
@@ -123,12 +150,28 @@ public class ValidatePostLogoutRedirectURI extends AbstractOIDCRpInitiatedLogout
         }
 
         if (!redirectURIValidationStrategy.test(profileRequestContext, requestedRedirectURI)) {
-            log.warn("{} Post logout redirection URI {} did not pass the validation", getLogPrefix(),
-                    requestedRedirectURI);
-            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
+            if (!ignoreInvalidPostLogoutRedirectUri) {
+                log.warn("{} Post logout redirection URI {} did not pass the validation", getLogPrefix(),
+                        requestedRedirectURI);
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
+            } else {
+                log.info("{} Ignoring failed post logout redirection URI validation for {}", getLogPrefix(),
+                        requestedRedirectURI);
+                removePostLogoutRedirectUri();
+            }
             return;
         }
         
         log.debug("{} Post logout redirection URI {} successfully validated", getLogPrefix(), requestedRedirectURI);
     }
+
+    /**
+     * Sets the URI into null via {@link OIDCRpInitiatedLogoutContext#setPostLogoutRedirectUri(URI)}
+     */
+    protected void removePostLogoutRedirectUri() {
+        log.debug("{} Set post logout redirection URI into null", getLogPrefix());
+        final OIDCRpInitiatedLogoutContext rpInitiatedLogoutContext = getRpInitiatedLogoutContext();
+        assert rpInitiatedLogoutContext != null;
+        rpInitiatedLogoutContext.setPostLogoutRedirectUri(null);
+    }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 5dbc86ee..98dee9b5 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -94,7 +94,8 @@
           p:frontChannelSuccess="%{idp.oidc.logout.frontChannelSuccess:false}"
           p:revokeTokens="%{idp.oidc.logout.revokeTokens:true}"
           p:requireIdTokenHint="%{idp.oidc.logout.requireIdTokenHint:true}"
-          p:encryptionOptional="%{idp.oidc.logout.encryptionOptional:true}"/>
+          p:encryptionOptional="%{idp.oidc.logout.encryptionOptional:true}"
+          p:ignoreInvalidPostLogoutRedirectUri="%{idp.oidc.logout.ignoreInvalidPostLogoutRedirectUri:false}"/>
 
     <bean id="OAUTH2.PAR" parent="OIDC.SSO" lazy-init="true"
         class="net.shibboleth.oidc.profile.oauth2.config.impl.DefaultOAuth2PushedAuthorizationRequestConfiguration"
@@ -431,6 +432,14 @@
                 <constructor-arg value="%{idp.oidc.logout.requireIdTokenHint:true}" />
             </bean>
         </property>
+        <property name="ignoreInvalidPostLogoutRedirectUriPredicate">
+            <bean class="net.shibboleth.shared.logic.PredicateSupport" factory-method="fromFunction">
+                <constructor-arg>
+                    <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="ignoreInvalidPostLogoutRedirectUri" />
+                </constructor-arg>
+                <constructor-arg value="%{idp.oidc.logout.ignoreInvalidPostLogoutRedirectUri:false}" />
+            </bean>
+        </property>
         <property name="logoutHintMatchingStrategyLookupStrategy">
             <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="logoutHintMatchingStrategy"
                 p:propertyType="#{T(java.util.function.BiPredicate)}"
diff --git a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
index 197b4f12..5899d690 100644
--- a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
@@ -146,6 +146,9 @@ idp.oidc.subject.salt = this_too_should_be_ch4ng3d
 # The offset to add to the 'exp' claim time for logout token claims set
 #idp.oidc.logout.logoutTokenExpiryOffset = PT30S
 
+# Whether to ignore invalid value for post_logout_redirect_uri parameter in the RP-initiated logout request.
+#idp.oidc.logout.ignoreInvalidPostLogoutRedirectUri = false
+
 # Settings for issue-registration-access-token flow
 #idp.oidc.admin.registration.logging = IssueRegistrationAccessToken
 #idp.oidc.admin.registration.nonBrowserSupported = true
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURITest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURITest.java
index bf1fd2e3..6ebf6888 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURITest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURITest.java
@@ -19,6 +19,7 @@ import java.net.URISyntaxException;
 import java.util.Set;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.testng.Assert;
@@ -40,15 +41,19 @@ public class ValidatePostLogoutRedirectURITest
 
     @Override
     protected ValidatePostLogoutRedirectURI constructAction() {
-        return constructAction(null);
+        return constructAction(null, null);
     }
 
     protected ValidatePostLogoutRedirectURI constructAction(
-            final Function<ProfileRequestContext,BiPredicate<URI, ProfileRequestContext>> customStrategy) {
+            final Function<ProfileRequestContext,BiPredicate<URI, ProfileRequestContext>> customStrategy,
+            final Predicate<ProfileRequestContext> ignoreInvalidPredicate) {
         final ValidatePostLogoutRedirectURI action = new ValidatePostLogoutRedirectURI();
         if (customStrategy != null) {
             action.setCustomPostLogoutRedirectUriValidationStrategyLookupStrategy(customStrategy);
         }
+        if (ignoreInvalidPredicate != null) {
+            action.setIgnoreInvalidPostLogoutRedirectUriPredicate(ignoreInvalidPredicate);
+        }
         try {
             action.initialize();
         } catch (final ComponentInitializationException e) {
@@ -68,9 +73,17 @@ public class ValidatePostLogoutRedirectURITest
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.INVALID_REDIRECT_URI);
     }
 
+    @Test
+    public void noRegisteredPostLogoutRedirectURis_ignoreInvalid() {
+        action = constructAction(null, prc -> true);
+        rpInitiatedLogoutContext.setPostLogoutRedirectUri(mockPostLogoutRedirectUri());
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertNull(rpInitiatedLogoutContext.getPostLogoutRedirectUri());
+    }
+
     @Test
     public void noRegisteredPostLogoutRedirectURis_overrideWithCustomPredicate() {
-        action = constructAction(p -> ((prc, uri) -> true));
+        action = constructAction(p -> ((prc, uri) -> true), null);
         rpInitiatedLogoutContext.setPostLogoutRedirectUri(mockPostLogoutRedirectUri());
         ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
     }
@@ -84,9 +97,20 @@ public class ValidatePostLogoutRedirectURITest
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.INVALID_REDIRECT_URI);
     }
 
+    @Test
+    public void noMatchingRegisteredPostLogoutRedirectURis_ignoreInvalid() throws URISyntaxException {
+        action = constructAction(null, prc -> true);
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setPostLogoutRedirectionURIs(Set.of(new URI("https://rp.example.com/notMatching")));
+        oidcMetadataContext.setClientInformation(new OIDCClientInformation(new ClientID("mockClientId"), metadata));
+        rpInitiatedLogoutContext.setPostLogoutRedirectUri(mockPostLogoutRedirectUri());
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertNull(rpInitiatedLogoutContext.getPostLogoutRedirectUri());
+    }
+
     @Test
     public void noMatchingRegisteredPostLogoutRedirectURis_overrideWithCustomPredicate() throws URISyntaxException {
-        action = constructAction(p -> ((prc, uri) -> true));
+        action = constructAction(p -> ((prc, uri) -> true), null);
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
         metadata.setPostLogoutRedirectionURIs(Set.of(new URI("https://rp.example.com/notMatching")));
         oidcMetadataContext.setClientInformation(new OIDCClientInformation(new ClientID("mockClientId"), metadata));
@@ -105,7 +129,7 @@ public class ValidatePostLogoutRedirectURITest
 
     @Test
     public void matchingRegisteredPostLogoutRedirectURis_overrideWithCustomPredicate() {
-        action = constructAction(p -> ((prc, uri) -> false));
+        action = constructAction(p -> ((prc, uri) -> false), null);
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
         metadata.setPostLogoutRedirectionURIs(Set.of(mockPostLogoutRedirectUri()));
         oidcMetadataContext.setClientInformation(new OIDCClientInformation(new ClientID("mockClientId"), metadata));
@@ -113,6 +137,17 @@ public class ValidatePostLogoutRedirectURITest
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.INVALID_REDIRECT_URI);
     }
 
+    @Test
+    public void matchingRegisteredPostLogoutRedirectURis_overrideWithCustomPredicate_ignoreInvalid() {
+        action = constructAction(p -> ((prc, uri) -> false), prc -> true);
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setPostLogoutRedirectionURIs(Set.of(mockPostLogoutRedirectUri()));
+        oidcMetadataContext.setClientInformation(new OIDCClientInformation(new ClientID("mockClientId"), metadata));
+        rpInitiatedLogoutContext.setPostLogoutRedirectUri(mockPostLogoutRedirectUri());
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertNull(rpInitiatedLogoutContext.getPostLogoutRedirectUri());
+    }
+
     protected URI mockPostLogoutRedirectUri() {
         try {
             return new URI("https://rp.example.com/postLogout");
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
index 54ec8951..15fbaeea 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
@@ -71,6 +71,7 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
     String sessionId = "mockSessionId";
     String subject = "mockSubject";
     String clientId = "mockClientId";
+    String clientIdIgnoreInvalidRedirectUri = "mockClientIdIgnoreInvalidRedirectUri";
     String clientSecret = "mockClientSecretmockClientSecretmockClientSecretmockClientSecretmockClientSecret";
     
     @Autowired
@@ -326,6 +327,40 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals("ErrorView", result.getOutcome().getId());
     }
 
+    @Test
+    public void testWithValidIdTokenHint_withPostLogoutRedirectUriNotStored_invalidUriIgnored() {
+        final String clientId = clientIdIgnoreInvalidRedirectUri;
+        final SignedJWT idTokenHint;
+        try {
+            idTokenHint = createPrivateKeyJWT(JWTClaimsSet.parse(
+                    getIdTokenHintPayload(issuer, subject, clientId, Instant.now().plusSeconds(300), Instant.now(),
+                    sessionId)),
+                    (RSAPrivateKey) loadRSSigningCredential().getPrivateKey(), JWSAlgorithm.RS256);
+        } catch (JOSEException | ParseException e) {
+            Assert.fail();
+            return;
+        }
+        setRequestParameters(List.of(new Pair<>("id_token_hint", idTokenHint.serialize()),
+                new Pair<>("post_logout_redirect_uri", postLogoutRedirectUri)));
+        request.setMethod("GET");
+        storeMetadata(storageService, clientId, clientSecret);
+
+        initializeThreadLocals();
+
+        final IdPSession session = buildIdPSessionWithSP(clientId);
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(isSessionValid(session));
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertFalse(result.isEnded());
+
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+        final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+        Assert.assertFalse(isSessionValid(session));
+        Assert.assertEquals(response.getStatus(), 200);
+        Assert.assertTrue(result2.isEnded());
+    }
+
     @Test
     public void testWithValidIdTokenHint_withPostLogoutRedirection() {
         final SignedJWT idTokenHint;
@@ -367,8 +402,12 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
         };
     }
 
-    @SuppressWarnings("null")
     protected IdPSession buildIdPSessionWithDefaultSP() {
+       return buildIdPSessionWithSP(clientId);
+    }
+
+    @SuppressWarnings("null")
+    protected IdPSession buildIdPSessionWithSP(final String clientId) {
        return buildIdPSession(sessionManager, new OIDCRPSession.Builder()
                .serviceId(clientId)
                .issuer(issuer)
@@ -418,6 +457,7 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
     @AfterMethod
     public void removeMetadata() throws IOException {
         removeMetadata(storageService, clientId);
+        removeMetadata(storageService, clientIdIgnoreInvalidRedirectUri);
     }
 
     protected boolean isSessionValid(final IdPSession session) {
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 367270e9..3ad00f80 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -274,6 +274,13 @@
                  </list>
             </property>
         </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdIgnoreInvalidRedirectUri">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.Logout.MDDriven" p:ignoreInvalidPostLogoutRedirectUri="true"/>
+                 </list>
+            </property>
+        </bean>
         <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLogoutFrontSuccessWithRevoke">
             <property name="profileConfigurations">
                  <list>

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


More information about the commits mailing list