[java-plugin-shibd] branch main updated: Add logout consumer flow support to core objects.

Codeberg noreply at shibboleth.net
Wed May 20 13:40:16 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-plugin-shibd.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd/commit/258bf7575777a352d2731fc21e99ac4617d2b4cc

The following commit(s) were added to refs/heads/main by this push:
     new 258bf75  Add logout consumer flow support to core objects.
258bf75 is described below

commit 258bf7575777a352d2731fc21e99ac4617d2b4cc
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Wed May 20 09:38:21 2026 -0400

    Add logout consumer flow support to core objects.
---
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   6 +-
 .../logout-consumer-beans.xml}                     |  10 +-
 .../sp/logout-consumer/logout-consumer-flow.xml    |  30 ++++++
 .../sp/token-consumer/token-consumer-beans.xml     |   2 +-
 .../sp/flows/LogoutConsumerFlowText.java           |  93 ++++++++++++++++
 .../main/java/net/shibboleth/sp/Application.java   |   9 ++
 .../shibboleth/sp/BasicProtocolSupportService.java |  32 ++++--
 .../net/shibboleth/sp/ProtocolSupportService.java  |  10 ++
 .../profile/BasicLogoutConsumerFlowDescriptor.java |  86 +++++++++++++++
 .../sp/profile/LogoutConsumerFlowDescriptor.java   |  45 ++++++++
 .../net/shibboleth/sp/impl/BasicApplication.java   |  57 ++++++++++
 .../impl/LogoutConsumerFlowDescriptorManager.java  |  41 +++++++
 .../sp/profile/impl/SelectLogoutConsumerFlow.java  | 120 +++++++++++++++++++++
 13 files changed, 528 insertions(+), 13 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 1795b0e..061a026 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -39,12 +39,16 @@
     <bean id="shibboleth.RemotedHttpServletRequestSupplier" class="net.shibboleth.sp.messaging.impl.RemotedHttpServletRequestSupplier" />
     <bean id="shibboleth.RemotedHttpServletResponseSupplier" class="net.shibboleth.sp.messaging.impl.RemotedlHttpServletResponseSupplier" />
 
-    <!-- Management bean to collect and expose TokenConsumerFlowDescriptors. -->
+    <!-- Management bean to collect and expose consumer flow descriptors. -->
     
     <bean id="shibboleth.sp.TokenConsumerFlowDescriptorManager"
         class="net.shibboleth.sp.profile.impl.TokenConsumerFlowDescriptorManager"
         p:components="#{getObject('shibboleth.sp.AvailableTokenConsumerFlows')}" />
 
+    <bean id="shibboleth.sp.LogoutConsumerFlowDescriptorManager"
+        class="net.shibboleth.sp.profile.impl.LogoutConsumerFlowDescriptorManager"
+        p:components="#{getObject('shibboleth.sp.AvailableLogoutConsumerFlows')}" />
+
     <bean id="shibboleth.sp.RemotedCookieManager" class="net.shibboleth.shared.net.CookieManager" lazy-init="true"
         p:guardSetAttribute="false"
         p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-consumer/logout-consumer-beans.xml
similarity index 72%
copy from sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml
copy to sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-consumer/logout-consumer-beans.xml
index c374136..a89a953 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-consumer/logout-consumer-beans.xml
@@ -7,11 +7,11 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
-    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/token-consumer" />
-    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.consumer:SPAgent.TokenConsumer}" />
+    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/logout-consumer" />
+    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.logout-consumer:SPAgent.LogoutConsumer}" />
     
-    <bean id="SelectTokenConsumerFlow"
-        class="net.shibboleth.sp.profile.impl.SelectTokenConsumerFlow" scope="prototype"
-        p:availableFlows="#{getObject('shibboleth.sp.TokenConsumerFlowDescriptorManager').getComponents()}" />
+    <bean id="SelectLogoutConsumerFlow"
+        class="net.shibboleth.sp.profile.impl.SelectLogoutConsumerFlow" scope="prototype"
+        p:availableFlows="#{getObject('shibboleth.sp.LogoutConsumerFlowDescriptorManager').getComponents()}" />
     
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-consumer/logout-consumer-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-consumer/logout-consumer-flow.xml
new file mode 100644
index 0000000..eeebca7
--- /dev/null
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-consumer/logout-consumer-flow.xml
@@ -0,0 +1,30 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="sp/application">
+
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to parent flow for authentication. -->
+        <transition on="proceed" to="AuthenticationSetup" />
+    </action-state>
+
+    <!-- Resume flow operation after set up by parent. -->
+    <action-state id="DoApplicationOperation">
+        <evaluate expression="SelectLogoutConsumerFlow" />
+        
+        <!-- Call a subflow with the same ID as the event. -->
+        <transition on="#{currentEvent.id.startsWith('sp/logout/consumer/')}" to="CallConsumerFlow" />
+    </action-state>
+
+    <subflow-state id="CallConsumerFlow" subflow="#{currentEvent.id}">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="EncodeAgentResponse" />
+    </subflow-state>
+    
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/logout-consumer/logout-consumer-beans.xml" />
+
+</flow>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml
index c374136..5c55e74 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml
@@ -8,7 +8,7 @@
     default-init-method="initialize" default-destroy-method="destroy">
 
     <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/token-consumer" />
-    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.consumer:SPAgent.TokenConsumer}" />
+    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.token-consumer:SPAgent.TokenConsumer}" />
     
     <bean id="SelectTokenConsumerFlow"
         class="net.shibboleth.sp.profile.impl.SelectTokenConsumerFlow" scope="prototype"
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutConsumerFlowText.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutConsumerFlowText.java
new file mode 100644
index 0000000..7624ec7
--- /dev/null
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutConsumerFlowText.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.flows;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * Unit test for the SP logout-consumer flow (basics only).
+ */
+public class LogoutConsumerFlowText extends AbstractSPFlowTest {
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "sp/logout-consumer";
+
+    protected LogoutConsumerFlowText() {
+        super(FLOW_ID);
+    }
+    
+    /**
+     * Test flow with no application specified.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testNoApplication() throws IOException {
+        setDefaultAuth();
+        setRequest("POST", new DDF(null));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+    }
+    
+    /**
+     * Test flow with invalid application ID.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testBadApplication() throws IOException {
+        setDefaultAuth();
+
+        final DDF input = new DDF(null).structure();
+        input.addmember(SPConstants.APPLICATION).string("foo");
+        setRequest("POST", input);
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+    }
+    
+    /**
+     * Test flow with no consumer subflows installed.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testNoFlows() throws IOException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(SPConstants.APPLICATION).string(APPLICATION_ID);
+        setRequest("POST", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
+    }
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
index e8e14d8..40c23c4 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
@@ -102,6 +102,15 @@ public interface Application extends RelyingPartyConfigurationResolver {
      */
     @Nonnull List<String> getLogoutInitiators(@Nullable final ProfileRequestContext profileRequestContext);
 
+    /**
+     * Gets an ordered list of logout consumer flows to attempt for this application.
+     * 
+     * @param profileRequestContext profile request context
+     *  
+     * @return ordered list of unprefixed flow IDs
+     */
+    @Nonnull List<String> getLogoutConsumers(@Nullable final ProfileRequestContext profileRequestContext);
+    
     /**
      * Gets the {@link StateManager} to use for thie application.
      * 
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java b/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java
index 6757095..ce13d31 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java
@@ -42,11 +42,14 @@ public class BasicProtocolSupportService extends AbstractIdentifiableInitializab
     /** Session Initiator flow IDs. */
     @Nonnull private List<String> sessionInitiators;
     
-    /** Consumer flow IDs. */
-    @Nonnull private List<String> consumers;
+    /** Token Consumer flow IDs. */
+    @Nonnull private List<String> tokenConsumers;
 
     /** Logout Initiator flow IDs. */
     @Nonnull private List<String> logoutInitiators;
+
+    /** Logout Consumer flow IDs. */
+    @Nonnull private List<String> logoutConsumers;
     
     /** Non-metadata-driven verified configurations. */
     @Nonnull private Collection<ProfileConfiguration> profileConfigurations;
@@ -63,8 +66,9 @@ public class BasicProtocolSupportService extends AbstractIdentifiableInitializab
     /** Constructor. */
     public BasicProtocolSupportService() {
         sessionInitiators = CollectionSupport.emptyList();
-        consumers = CollectionSupport.emptyList();
+        tokenConsumers = CollectionSupport.emptyList();
         logoutInitiators = CollectionSupport.emptyList();
+        logoutConsumers = CollectionSupport.emptyList();
         profileConfigurations = CollectionSupport.emptySet();
         metadataDrivenProfileConfigurations = CollectionSupport.emptySet();
         unverifiedProfileConfigurations = CollectionSupport.emptySet();
@@ -105,18 +109,18 @@ public class BasicProtocolSupportService extends AbstractIdentifiableInitializab
 
     /** {@inheritDoc} */
     @Nonnull @Unmodifiable @NotLive public List<String> getTokenConsumers() {
-        return consumers;
+        return tokenConsumers;
     }
     
     /**
      * Sets the ordered list of token consumer flow IDs to install.
      * 
-     * @param flows ordered list of unprefixed consymer flow IDs
+     * @param flows ordered list of unprefixed token consumer flow IDs
      */
     public void setTokenConsumers(@Nullable final List<String> flows) {
         checkSetterPreconditions();
         
-        consumers = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(flows));
+        tokenConsumers = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(flows));
     }
 
     /** {@inheritDoc} */
@@ -135,6 +139,22 @@ public class BasicProtocolSupportService extends AbstractIdentifiableInitializab
         logoutInitiators = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(flows));
     }
     
+    /** {@inheritDoc} */
+    @Nonnull @Unmodifiable @NotLive public List<String> getLogoutConsumers() {
+        return logoutConsumers;
+    }
+    
+    /**
+     * Sets the ordered list of logout consumer flow IDs to install.
+     * 
+     * @param flows ordered list of unprefixed logout consumer flow IDs
+     */
+    public void setLogoutConsumers(@Nullable final List<String> flows) {
+        checkSetterPreconditions();
+        
+        logoutConsumers = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(flows));
+    }
+    
     /** {@inheritDoc} */
     @Nonnull @Unmodifiable @NotLive public Collection<ProfileConfiguration> getDefaultProfileConfigurations(
             boolean metadataDriven) {
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java b/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java
index 4e08daa..7c428f6 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java
@@ -65,6 +65,16 @@ public interface ProtocolSupportService extends IdentifiedComponent, Initializab
      * @return ordered list of unprefixed flow IDs
      */
     @Nonnull @Unmodifiable @NotLive List<String> getLogoutInitiators();
+
+    /**
+     * Gets a default-ordered list of logout consumer flows supported by this protocol plugin.
+     *  
+     * <p>The default order should reflect what "most" deployers would expect to use when adding
+     * support for this plugin to their system.</p>
+     * 
+     * @return ordered list of unprefixed flow IDs
+     */
+    @Nonnull @Unmodifiable @NotLive List<String> getLogoutConsumers();
     
     /**
      * Gets the collection of default {@link ProfileConfiguration} objects supplied by this protocol plugin.
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/BasicLogoutConsumerFlowDescriptor.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/BasicLogoutConsumerFlowDescriptor.java
new file mode 100644
index 0000000..6344582
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/BasicLogoutConsumerFlowDescriptor.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.profile;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+
+/**
+ * Basic implementation of a descriptor for a logout consumer flow.
+ */
+public class BasicLogoutConsumerFlowDescriptor extends AbstractIdentifiableInitializableComponent
+        implements LogoutConsumerFlowDescriptor {
+    
+    /** Whether this flow supports a particular request. */
+    @Nonnull private Predicate<ProfileRequestContext> activationCondition;
+    
+    /** Constructor. */
+    public BasicLogoutConsumerFlowDescriptor() {
+        activationCondition = PredicateSupport.alwaysFalse();
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull public Predicate<ProfileRequestContext> getActivationCondition() {
+        return activationCondition;
+    }
+    
+    /**
+     * Set activation condition to determine whether this flow supports a request.
+     * 
+     * @param condition condition to use
+     */
+    public void setActivationCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        activationCondition = Constraint.isNotNull(condition, "Activation condition cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return ensureId().hashCode();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj instanceof BasicLogoutConsumerFlowDescriptor) {
+            return ensureId().equals(((BasicLogoutConsumerFlowDescriptor) obj).getId());
+        }
+
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("flowId", getId())
+                .toString();
+    }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/LogoutConsumerFlowDescriptor.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/LogoutConsumerFlowDescriptor.java
new file mode 100644
index 0000000..0c02348
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/LogoutConsumerFlowDescriptor.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.profile;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.profile.FlowDescriptor;
+
+/**
+ * A descriptor for a logout consumer subflow.
+ * 
+ * <p>Logout consumers are the portion of the SP responsible for consuming logout protocol
+ * responses that attempt to implement single logout. In SAML, this would be the
+ * "single logout service".</p>
+ * 
+ * <p>The primary need for this is to associate each subflow with a condition that can
+ * determine whether a given request is handled by a given subflow. For example, determining
+ * the protocol in use, the parametes included in the request, etc.</p>
+ */
+public interface LogoutConsumerFlowDescriptor extends FlowDescriptor {
+
+    /**
+     * Gets the activation condition to run to determine whether this flow can handle the request.
+     * 
+     * @return a condition governing use of this flow
+     */
+    @Nonnull Predicate<ProfileRequestContext> getActivationCondition();
+    
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
index 4c16a5c..4d007e4 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
@@ -95,6 +95,9 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
 
     /** Logout initiator list lookup strategy. */
     @NonnullAfterInit private Function<ProfileRequestContext,List<String>> logoutInitiatorLookupStrategy;
+
+    /** Logout consumer list lookup strategy. */
+    @NonnullAfterInit private Function<ProfileRequestContext,List<String>> logoutConsumerLookupStrategy;
     
     /** State management. */
     @Nullable private StateManager stateManager;
@@ -377,7 +380,49 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
         logoutInitiatorLookupStrategy = Constraint.isNotNull(strategy,
                 "Logout initiators lookup strategy cannot be null");
     }
+
+    /** {@inheritDoc} */
+    @Nonnull public List<String> getLogoutConsumers(@Nullable final ProfileRequestContext profileRequestContext) {
+        final List<String> ret = logoutConsumerLookupStrategy.apply(profileRequestContext);
+        if (ret != null && !ret.isEmpty()) {
+            return ret;
+        }
+
+        if (parent != null && allowInheritance) {
+            return parent.getLogoutConsumers(profileRequestContext);
+        }
+        
+        return CollectionSupport.emptyList(); 
+    }
+    
+    /**
+     * Sets the ordered collection of unprefixed logout consumer flow IDs to use for this application.
+     * 
+     * @param consumers ordered collection of unprefixed logout consumer flow IDs
+     */
+    public void setLogoutConsumers(@Nullable final Collection<String> consumers) {
+        checkSetterPreconditions();
         
+        if (consumers != null) {
+            logoutConsumerLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToList(StringSupport.normalizeStringCollection(consumers)));
+        } else {
+            logoutConsumerLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        }
+    }
+    
+    /**
+     * Sets the lookup strategy for the ordered collection of unprefixed logout consumer flow IDs
+     * to use for this application.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutConsumersLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        checkSetterPreconditions();
+        
+        logoutConsumerLookupStrategy = Constraint.isNotNull(strategy, "Logout consumers lookup strategy cannot be null");
+    }
+    
     /** {@inheritDoc} */
     @Nonnull public StateManager getStateManager() {
         checkComponentActive();
@@ -608,6 +653,9 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
             if (logoutInitiatorLookupStrategy == null) {
                 logoutInitiatorLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
             }
+            if (logoutConsumerLookupStrategy == null) {
+                logoutConsumerLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+            }
             return;
         }
         
@@ -649,6 +697,15 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
             }
             logoutInitiatorLookupStrategy = FunctionSupport.constant(CollectionSupport.copyToList(initiators));
         }
+        
+        if (logoutConsumerLookupStrategy == null) {
+            final ArrayList<String> consumers = new ArrayList<>();
+            for (final ProtocolSupportService service : protocolServiceManager.getComponents()) {
+                assert service != null;
+                consumers.addAll(StringSupport.normalizeStringCollection(service.getLogoutConsumers()));
+            }
+            logoutConsumerLookupStrategy = FunctionSupport.constant(CollectionSupport.copyToList(consumers));
+        }
     }
     
     /** {@inheritDoc} */
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/LogoutConsumerFlowDescriptorManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/LogoutConsumerFlowDescriptorManager.java
new file mode 100644
index 0000000..d831738
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/LogoutConsumerFlowDescriptorManager.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.profile.impl;
+
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import net.shibboleth.shared.spring.config.IdentifiedComponentManager;
+import net.shibboleth.sp.profile.LogoutConsumerFlowDescriptor;
+
+/**
+ * Manager of {@link LogoutConsumerFlowDescriptor} objects.
+ */
+public class LogoutConsumerFlowDescriptorManager extends IdentifiedComponentManager<LogoutConsumerFlowDescriptor> {
+
+    /**
+     * Constructor.
+     *
+     * @param freeObjects  free-standing objects
+     */
+    @Autowired
+    public LogoutConsumerFlowDescriptorManager(@Nullable final Collection<LogoutConsumerFlowDescriptor> freeObjects) {
+        super(freeObjects);
+    }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/SelectLogoutConsumerFlow.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/SelectLogoutConsumerFlow.java
new file mode 100644
index 0000000..af883b5
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/SelectLogoutConsumerFlow.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.profile.impl;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.LogoutConsumerFlowDescriptor;
+
+/**
+ * Action that checks all enabled logout consumer flows and returns the flow ID of the first
+ * enabled flow indicating it can handle the request.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#NO_POTENTIAL_FLOW}
+ * @event selected logout consumer flow ID
+ */
+public class SelectLogoutConsumerFlow extends AbstractApplicationAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(SelectLogoutConsumerFlow.class);
+    
+    /** Map of available consumer flows. */
+    @Nonnull private Map<String,LogoutConsumerFlowDescriptor> availableFlows;
+
+    /** Constructor. */
+    public SelectLogoutConsumerFlow() {
+        availableFlows = CollectionSupport.emptyMap();
+    }
+    
+    /**
+     * Sets the available token consumer flows to consider.
+     * 
+     * @param flows available flows
+     */
+    public void setAvailableFlows(@Nonnull final Collection<LogoutConsumerFlowDescriptor> flows) {
+        checkSetterPreconditions();
+        
+        availableFlows = flows.stream()
+            .collect(CollectionSupport.nonnullCollector(
+                    Collectors.toUnmodifiableMap(LogoutConsumerFlowDescriptor::getId, Function.identity()))).get();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        if (ensureAgentRequestContext().getRemotedHttpServletRequest() == null) {
+            log.warn("No remoted HttpServletRequest available, malformed agent request?", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_POTENTIAL_FLOW);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        // We do the crazy stuff to accomodate flow activation conditions evaluating the servlet request.
+        try {
+            RemotedHttpServletRequestResponseContext.loadCurrent(ensureAgentRequestContext().getRemotedHttpServletRequest(),
+                    ensureAgentRequestContext().getRemotedHttpServletResponse());
+
+            final List<String> flows = ensureApplication().getLogoutConsumers(profileRequestContext);
+            for (final String flowId : flows) {
+                assert flowId != null;
+                final LogoutConsumerFlowDescriptor flowDescriptor = availableFlows.get("sp/logout/consumer/" + flowId);
+                if (flowDescriptor != null) {
+                    if (flowDescriptor.getActivationCondition().test(profileRequestContext)) {
+                        log.debug("{} Logout consumer flow {} handling request", getLogPrefix(), flowId);
+                        ActionSupport.buildEvent(profileRequestContext, "sp/logout/consumer/" + flowId);
+                        return;
+                    } else {
+                        log.debug("{} Logout consumer flow {} cannot handle request", getLogPrefix(), flowId);
+                    }
+                } else {
+                    log.warn("{} Logout consumer flow {} is not available", getLogPrefix(), flowId);
+                }
+            }
+        } finally {
+            RemotedHttpServletRequestResponseContext.clearCurrent();
+        }
+        
+        log.warn("{} No logout consumer flows were able to handle request", getLogPrefix());
+        ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_POTENTIAL_FLOW);
+    }
+
+}
\ No newline at end of file

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


More information about the commits mailing list