[java-idp-oidc] 01/02: JOIDC-275 - OIDC RP-Initiated logout goes to local error if session doesn't exists

Codeberg noreply at shibboleth.net
Mon Jul 13 10:38:27 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/4ab6e454fce20df9bc3e3486b9657ea38819c448

commit 4ab6e454fce20df9bc3e3486b9657ea38819c448
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Jun 25 13:37:23 2026 +0300

    JOIDC-275 - OIDC RP-Initiated logout goes to local error if session doesn't exists
    
    https://shibboleth.atlassian.net/browse/JOIDC-275
    
    New property 'idp.oidc.logout.requireSessionCondition' can be used for wiring a condition bean (Predicate<ProfileRequestContext>)
    - If it returns 'false' (which is the default), then the sequence proceeds in the same way when matching session exists or not
    - If it returns 'true', then SessionNotFound is published when the session does not exist and  the sequence ends up to the error view
---
 .../impl/ProcessRpInitiatedLogoutRequest.java      | 26 +++++++-
 .../flows/oidc/end-session/end-session-beans.xml   |  3 +-
 .../idp/plugin/oidc/op/conf/oidc.properties        |  5 +-
 .../impl/ProcessRpInitiatedLogoutRequestTest.java  | 73 +++++++++++++++++++---
 .../net/shibboleth/idp/module/conf/oidc.properties |  4 +-
 5 files changed, 97 insertions(+), 14 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequest.java
index b0725171..29c5b4a9 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequest.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequest.java
@@ -21,6 +21,7 @@ import java.text.ParseException;
 import java.util.Iterator;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 
@@ -106,6 +107,9 @@ public class ProcessRpInitiatedLogoutRequest extends AbstractOIDCRpInitiatedLogo
     @Nonnull
     private Function<ProfileRequestContext,BiPredicate<String,SPSession>> logoutHintMatchingStrategyLookupStrategy;
 
+    /** Condition for requiring matching session to be found. */
+    @Nonnull private Predicate<ProfileRequestContext> requireSessionCondition;
+
     /**
      * Constructor.
      */
@@ -144,6 +148,7 @@ public class ProcessRpInitiatedLogoutRequest extends AbstractOIDCRpInitiatedLogo
         };
         
         logoutHintMatchingStrategyLookupStrategy = new LogoutHintMatchingStrategyLookupFunction();
+        requireSessionCondition = PredicateSupport.alwaysFalse();
     }
 
     /**
@@ -226,6 +231,18 @@ public class ProcessRpInitiatedLogoutRequest extends AbstractOIDCRpInitiatedLogo
                 "LogoutHintMatchingStrategy lookup strategy cannot be null");
     }
 
+    /**
+     * Set the condition for requiring matching session to be found.
+     * 
+     * @param predicate condition
+     * 
+     * @since 4.4.0
+     */
+    public void setRequireSessionCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        requireSessionCondition = Constraint.isNotNull(predicate, "RequireSessionCondition cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -241,6 +258,7 @@ public class ProcessRpInitiatedLogoutRequest extends AbstractOIDCRpInitiatedLogo
  // Checkstyle: CyclomaticComplexity|ReturnCount OFF
     /** {@inheritDoc} */
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final boolean requireSession = requireSessionCondition.test(profileRequestContext);
         try {
             final Iterable<IdPSession> sessions =
                     sessionResolver.resolve(sessionResolverCriteriaStrategy.apply(profileRequestContext));
@@ -295,7 +313,9 @@ public class ProcessRpInitiatedLogoutRequest extends AbstractOIDCRpInitiatedLogo
             
             if (logoutCtx == null) {
                 log.info("{} No active session(s) found matching LogoutRequest", getLogPrefix());
-                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.SESSION_NOT_FOUND);
+                if (requireSession) {
+                    ActionSupport.buildEvent(profileRequestContext, OidcEventIds.SESSION_NOT_FOUND);
+                }
             } else if (logoutCtx.getIdPSessions().size() == 1) {
                 final SessionContext sessionCtx = sessionContextCreationStrategy.apply(profileRequestContext);
                 if (sessionCtx != null) {
@@ -305,7 +325,9 @@ public class ProcessRpInitiatedLogoutRequest extends AbstractOIDCRpInitiatedLogo
 
         } catch (final ResolverException e) {
             log.error("{} Error resolving matching session(s)", getLogPrefix(), e);
-            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.SESSION_NOT_FOUND);
+            if (requireSession) {
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.SESSION_NOT_FOUND);
+            }
         }
     }
 // Checkstyle: CyclomaticComplexity|ReturnCount ON
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-beans.xml
index 42db3a83..7682bade 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-beans.xml
@@ -214,7 +214,8 @@
         scope="prototype" />
 
     <bean id="ProcessRpInitiatedLogoutRequest" class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.ProcessRpInitiatedLogoutRequest"
-        scope="prototype" p:sessionResolver-ref="shibboleth.SessionManager"/>
+        scope="prototype" p:sessionResolver-ref="shibboleth.SessionManager"
+        p:requireSessionCondition-ref="%{idp.oidc.logout.requireSessionCondition:shibboleth.Conditions.FALSE}"/>
 
     <!--TODO extractors --><bean id="LogoutPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
         p:fieldExtractors="#{getObject('shibboleth.LogoutRequestAuditExtractors') ?: getObject('shibboleth.DefaultLogoutRequestAuditExtractors')}" />
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 798dae56..b428b3d5 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
@@ -143,6 +143,9 @@ idp.oidc.subject.salt = this_too_should_be_ch4ng3d
 # Bean (BiPredicate<String,SPSession>) used for matching logout hint to the SP session
 #idp.oidc.logout.logoutHintMatchingStrategy = 
 
+# Condition whether to require matching session to be found within RP-initiated logout request to proceed without errors.
+#idp.oidc.logout.requireSessionCondition = shibboleth.Conditions.FALSE
+
 # Settings for issue-registration-access-token flow
 #idp.oidc.admin.registration.logging = IssueRegistrationAccessToken
 #idp.oidc.admin.registration.nonBrowserSupported = true
@@ -213,4 +216,4 @@ idp.oidc.subject.salt = this_too_should_be_ch4ng3d
 
 # Set to true to enable strict scope validation, i.e. unallowed requested scope is considered as an error (defaults to false)
 # Affects OIDC.SSO and OAUTH2.Token profile configurations
-#idp.oidc.strictScopeValidation = true
\ No newline at end of file
+#idp.oidc.strictScopeValidation = true
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequestTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequestTest.java
index da8f5e71..dfbebef2 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequestTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequestTest.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.Set;
 import java.util.function.BiPredicate;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mockito;
@@ -42,6 +43,7 @@ import net.shibboleth.idp.session.criterion.HttpServletRequestCriterion;
 import net.shibboleth.idp.session.criterion.SPSessionCriterion;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.resolver.CriteriaSet;
 import net.shibboleth.shared.resolver.ResolverException;
 
@@ -62,13 +64,15 @@ public class ProcessRpInitiatedLogoutRequestTest
 
     @Override
     protected ProcessRpInitiatedLogoutRequest constructAction() {
-        return constructAction(null);
+        return constructAction(null, PredicateSupport.alwaysTrue());
     }
 
-    protected ProcessRpInitiatedLogoutRequest constructAction(final String clientId) {
+    protected ProcessRpInitiatedLogoutRequest constructAction(final String clientId,
+            final Predicate<ProfileRequestContext> requireSession) {
         final ProcessRpInitiatedLogoutRequest action = new ProcessRpInitiatedLogoutRequest();
         action.setSessionResolver(sessionResolver);
         action.setLogoutHintMatchingStrategyLookupStrategy(logoutHintPredicate);
+        action.setRequireSessionCondition(requireSession);
         if (clientId != null) {
             action.setIssuerLookupStrategy(prc -> new ClientID(clientId));
         }
@@ -82,16 +86,25 @@ public class ProcessRpInitiatedLogoutRequestTest
 
     @Test
     public void testNoSPSessions() throws ResolverException {
-        action = constructAction(clientId);
+        action = constructAction(clientId, PredicateSupport.alwaysTrue());
         rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
         IdPSession session = Mockito.mock(IdPSession.class);
         initializeSessionResolver(session);
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.SESSION_NOT_FOUND);
     }
 
+    @Test
+    public void testNoSPSessions_notRequired() throws ResolverException {
+        action = constructAction(clientId, PredicateSupport.alwaysFalse());
+        rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
+        IdPSession session = Mockito.mock(IdPSession.class);
+        initializeSessionResolver(session);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+    }
+
     @Test
     public void testNoOIDCRPSessions() throws ResolverException {
-        action = constructAction(clientId);
+        action = constructAction(clientId, PredicateSupport.alwaysTrue());
         rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
         IdPSession session = Mockito.mock(IdPSession.class);
         Mockito.when(session.getSPSessions()).thenReturn(Set.of(new BasicSPSession("id", Instant.now(),
@@ -100,9 +113,20 @@ public class ProcessRpInitiatedLogoutRequestTest
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.SESSION_NOT_FOUND);
     }
 
+    @Test
+    public void testNoOIDCRPSessions_notRequired() throws ResolverException {
+        action = constructAction(clientId, PredicateSupport.alwaysFalse());
+        rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
+        IdPSession session = Mockito.mock(IdPSession.class);
+        Mockito.when(session.getSPSessions()).thenReturn(Set.of(new BasicSPSession("id", Instant.now(),
+                Instant.now().plusSeconds(600))));
+        initializeSessionResolver(session);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+    }
+
     @Test
     public void testNoMatchingClientIdInOIDCRPSessions() throws ResolverException {
-        action = constructAction(clientId);
+        action = constructAction(clientId, PredicateSupport.alwaysTrue());
         rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
         IdPSession session = Mockito.mock(IdPSession.class);
         Mockito.when(session.getSPSessions()).thenReturn(Set.of(mockRpSession("wrongClientId", issuer, subject)));
@@ -110,9 +134,19 @@ public class ProcessRpInitiatedLogoutRequestTest
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.SESSION_NOT_FOUND);
     }
 
+    @Test
+    public void testNoMatchingClientIdInOIDCRPSessions_notRequired() throws ResolverException {
+        action = constructAction(clientId, PredicateSupport.alwaysFalse());
+        rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
+        IdPSession session = Mockito.mock(IdPSession.class);
+        Mockito.when(session.getSPSessions()).thenReturn(Set.of(mockRpSession("wrongClientId", issuer, subject)));
+        initializeSessionResolver(session);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+    }
+
     @Test
     public void testNoMatchingSubjectInOIDCRPSessions() throws ResolverException {
-        action = constructAction(clientId);
+        action = constructAction(clientId, PredicateSupport.alwaysTrue());
         rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
         IdPSession session = Mockito.mock(IdPSession.class);
         Mockito.when(session.getSPSessions()).thenReturn(Set.of(mockRpSession(clientId, issuer, "wrongSubject")));
@@ -120,9 +154,19 @@ public class ProcessRpInitiatedLogoutRequestTest
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.SESSION_NOT_FOUND);
     }
 
+    @Test
+    public void testNoMatchingSubjectInOIDCRPSessions_notRequired() throws ResolverException {
+        action = constructAction(clientId, PredicateSupport.alwaysFalse());
+        rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
+        IdPSession session = Mockito.mock(IdPSession.class);
+        Mockito.when(session.getSPSessions()).thenReturn(Set.of(mockRpSession(clientId, issuer, "wrongSubject")));
+        initializeSessionResolver(session);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+    }
+
     @Test
     public void testSessionMatchedViaIdTokenHint() throws ResolverException {
-        action = constructAction(clientId);
+        action = constructAction(clientId, PredicateSupport.alwaysTrue());
         rpInitiatedLogoutContext.setProcessedIdTokenHint(mockJwtWithSessionId(sessionId));
         IdPSession session = Mockito.mock(IdPSession.class);
         Mockito.when(session.getSPSessions()).thenReturn(Set.of(mockRpSession(clientId, issuer, subject)));
@@ -132,7 +176,7 @@ public class ProcessRpInitiatedLogoutRequestTest
 
     @Test
     public void testSessionNotMatchedViaLogoutHint() throws ResolverException {
-        action = constructAction(clientId);
+        action = constructAction(clientId, PredicateSupport.alwaysTrue());
         rpInitiatedLogoutContext.setLogoutHint(sessionId);
         IdPSession session = Mockito.mock(IdPSession.class);
         Mockito.when(logoutHintPredicate.apply(Mockito.any())).thenReturn((str, ses) -> false);
@@ -141,9 +185,20 @@ public class ProcessRpInitiatedLogoutRequestTest
         ActionTestingSupport.assertEvent(action.execute(requestContext), OidcEventIds.SESSION_NOT_FOUND);
     }
 
+    @Test
+    public void testSessionNotMatchedViaLogoutHint_notRequired() throws ResolverException {
+        action = constructAction(clientId, PredicateSupport.alwaysFalse());
+        rpInitiatedLogoutContext.setLogoutHint(sessionId);
+        IdPSession session = Mockito.mock(IdPSession.class);
+        Mockito.when(logoutHintPredicate.apply(Mockito.any())).thenReturn((str, ses) -> false);
+        Mockito.when(session.getSPSessions()).thenReturn(Set.of(mockRpSession(clientId, issuer, subject)));
+        initializeSessionResolver(session);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+    }
+
     @Test
     public void testSessionMatchedViaLogoutHint() throws ResolverException {
-        action = constructAction(clientId);
+        action = constructAction(clientId, PredicateSupport.alwaysTrue());
         rpInitiatedLogoutContext.setLogoutHint(sessionId);
         IdPSession session = Mockito.mock(IdPSession.class);
         Mockito.when(logoutHintPredicate.apply(Mockito.any())).thenReturn((str, ses) -> true);
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
index 21d2f5d7..7e19f64d 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
@@ -32,4 +32,6 @@ idp.authn.Password.supportedPrincipals = \
     saml1/urn:oasis:names:tc:SAML:1.0:am:password, \
     oidc/password
 
-idp.authn.OAuth2Client.audit.format = %a|%T|%SP|%I|%s|%AF|%CV|%u|%tu|%AR|%UA|%iss|%sub|%exp|%iat|%typ
\ No newline at end of file
+idp.authn.OAuth2Client.audit.format = %a|%T|%SP|%I|%s|%AF|%CV|%u|%tu|%AR|%UA|%iss|%sub|%exp|%iat|%typ
+
+idp.oidc.logout.requireSessionCondition = shibboleth.Conditions.TRUE

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


More information about the commits mailing list