[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