[java-identity-provider] branch master updated: IDP-964 - Support SAML logout propagation via SOAP

Scott Cantor cantor.2 at osu.edu
Wed Nov 28 11:03:24 EST 2018


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

scantor pushed a commit to branch master
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=ba1c5a259b1daea129e2d320a46519d2a3bcdd06

The following commit(s) were added to refs/heads/master by this push:
       new  ba1c5a2   IDP-964 - Support SAML logout propagation via SOAP
ba1c5a2 is described below

commit ba1c5a259b1daea129e2d320a46519d2a3bcdd06
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Nov 28 11:03:22 2018 -0500

    IDP-964 - Support SAML logout propagation via SOAP
    
    https://issues.shibboleth.net/jira/browse/IDP-964
    
    Full SAML SOAP logout with propagation.
    Design changes in outbound binding mgmt to accomodate.
---
 .../impl/AbstractOutgoingSamlMessageAction.java    |  44 ++++++++-
 .../src/main/resources/system/conf/utilities.xml   |   3 +
 .../flows/cas/samlValidate/samlValidate-beans.xml  |  18 ++--
 .../flows/logout/propagation/saml2-beans.xml       |  15 ++-
 .../system/flows/logout/propagation/saml2-flow.xml |   7 +-
 .../system/flows/saml/saml-abstract-beans.xml      |   5 +-
 .../flows/saml/saml1/artifact-resolution-beans.xml |   4 +-
 .../flows/saml/saml1/attribute-query-beans.xml     |   4 +-
 .../system/flows/saml/saml1/common-beans.xml       |   7 +-
 .../flows/saml/saml1/sso-unsolicited-beans.xml     |   6 +-
 .../flows/saml/saml2/artifact-resolution-beans.xml |   4 +-
 .../flows/saml/saml2/attribute-query-beans.xml     |   4 +-
 .../system/flows/saml/saml2/common-beans.xml       |  17 +---
 .../system/flows/saml/saml2/idwsf-ssos-beans.xml   |   4 +-
 .../system/flows/saml/saml2/slo-back-beans.xml     |  48 ++++++---
 .../system/flows/saml/saml2/slo-back-flow.xml      |  69 +++++++++++--
 .../flows/saml/saml2/slo-front-abstract-beans.xml  |   4 +-
 .../system/flows/saml/saml2/sso-artifact-beans.xml |   4 +-
 .../system/flows/saml/saml2/sso-ecp-beans.xml      |   6 +-
 .../system/flows/saml/saml2/sso-post-beans.xml     |   4 +-
 .../flows/saml/saml2/sso-post-simplesign-beans.xml |   4 +-
 .../system/flows/saml/saml2/sso-redirect-beans.xml |   4 +-
 .../flows/saml/saml2/sso-unsolicited-beans.xml     |   4 +-
 .../resources/metadata/example-sp123-metadata.xml  |   7 ++
 .../InitializeOutboundMessageContextForError.java  |  15 ++-
 .../impl/PopulateBindingAndEndpointContexts.java   |  37 ++++---
 .../impl/SpringAwareMessageEncoderFactory.java     |  59 +++--------
 .../saml/saml2/profile/impl/SOAPLogoutRequest.java |  13 ++-
 .../session/impl/PrepareInboundMessageContext.java |   2 +-
 ...itializeOutboundMessageContextForErrorTest.java |   6 +-
 .../PopulateBindingAndEndpointContextsTest.java    |   9 +-
 .../LogoutContextSessionLookupFunction.java        |  53 ++++++++++
 .../impl/PopulateLogoutPropagationContext.java     |  67 +++++++++----
 .../impl/RestoreProfileRequestContextTree.java     | 108 +++++++++++++++++++++
 .../impl/SaveProfileRequestContextTree.java        | 105 ++++++++++++++++++++
 .../session/impl/SelectLogoutPropagationFlow.java  |   9 +-
 36 files changed, 606 insertions(+), 173 deletions(-)

diff --git a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/AbstractOutgoingSamlMessageAction.java b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/AbstractOutgoingSamlMessageAction.java
index 607ff79..78977ae 100644
--- a/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/AbstractOutgoingSamlMessageAction.java
+++ b/idp-cas-impl/src/main/java/net/shibboleth/idp/cas/flow/impl/AbstractOutgoingSamlMessageAction.java
@@ -24,14 +24,20 @@ import net.shibboleth.idp.cas.protocol.ProtocolError;
 import net.shibboleth.idp.cas.protocol.TicketValidationRequest;
 import net.shibboleth.idp.cas.protocol.TicketValidationResponse;
 import net.shibboleth.idp.profile.ActionSupport;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.core.xml.util.XMLObjectSupport;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.saml.common.SAMLObject;
 import org.opensaml.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.common.binding.BindingDescriptor;
 import org.opensaml.saml.common.messaging.context.SAMLBindingContext;
-import org.opensaml.saml.common.xml.SAMLConstants;
 import org.opensaml.saml.saml1.core.Response;
 import org.opensaml.soap.messaging.context.SOAP11Context;
 import org.opensaml.soap.soap11.Body;
@@ -49,13 +55,42 @@ public abstract class AbstractOutgoingSamlMessageAction extends
         AbstractCASProtocolAction<TicketValidationRequest, TicketValidationResponse> {
 
     /** CAS namespace. */
-    protected static final String NAMESPACE = "http://www.ja-sig.org/products/cas/";
+    @Nonnull @NotEmpty protected static final String NAMESPACE = "http://www.ja-sig.org/products/cas/";
     
     /** SOAP envelope needed for old/broken CAS clients. */
+    @Nonnull
     private QName envelopeName = new QName(SOAPConstants.SOAP11_NS, Envelope.DEFAULT_ELEMENT_LOCAL_NAME, "SOAP-ENV");
 
     /** SOAP body needed for old/broken CAS clients. */
-    private QName bodyName = new QName(SOAPConstants.SOAP11_NS, Body.DEFAULT_ELEMENT_LOCAL_NAME, "SOAP-ENV");
+    @Nonnull private QName bodyName = new QName(SOAPConstants.SOAP11_NS, Body.DEFAULT_ELEMENT_LOCAL_NAME, "SOAP-ENV");
+    
+    /** Descriptor for outgoing SOAP binding. */
+    @NonnullAfterInit private BindingDescriptor outgoingBinding;
+    
+    /**
+     * Set the {@link BindingDescriptor} describing the outbound binding to use.
+     * 
+     * @param descriptor the descriptor
+     * 
+     * @since 4.0.0
+     */
+    public void setOutgoingBinding(@Nonnull final BindingDescriptor descriptor) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        outgoingBinding = Constraint.isNotNull(descriptor, "Outgoing BindingDescriptor cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (outgoingBinding == null) {
+            throw new ComponentInitializationException("Outgoing BindingDescriptor cannot be null");
+        }
+    }
+
+
 
     /**
      * Build the SAML object.
@@ -84,7 +119,8 @@ public abstract class AbstractOutgoingSamlMessageAction extends
             return ProtocolError.IllegalState.event(this);
         }
         final SAMLBindingContext bindingContext = new SAMLBindingContext();
-        bindingContext.setBindingUri(SAMLConstants.SAML1_SOAP11_BINDING_URI);
+        bindingContext.setBindingUri(outgoingBinding.getId());
+        bindingContext.setBindingDescriptor(outgoingBinding);
         msgContext.addSubcontext(bindingContext);
 
         // Ensure message uses SOAP-ENV ns prefix required by old/broken CAS clients
diff --git a/idp-conf/src/main/resources/system/conf/utilities.xml b/idp-conf/src/main/resources/system/conf/utilities.xml
index f95daea..e15e525 100644
--- a/idp-conf/src/main/resources/system/conf/utilities.xml
+++ b/idp-conf/src/main/resources/system/conf/utilities.xml
@@ -140,6 +140,9 @@
     <bean id="shibboleth.ChildLookup.SessionContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.idp.session.context.SessionContext) }" />
+    <bean id="shibboleth.ChildLookup.LogoutContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.idp.session.context.LogoutContext) }" />
         
     <bean id="shibboleth.ChildLookup.SAMLBindingContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
diff --git a/idp-conf/src/main/resources/system/flows/cas/samlValidate/samlValidate-beans.xml b/idp-conf/src/main/resources/system/flows/cas/samlValidate/samlValidate-beans.xml
index b23d03c..5caa20b 100644
--- a/idp-conf/src/main/resources/system/flows/cas/samlValidate/samlValidate-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/cas/samlValidate/samlValidate-beans.xml
@@ -13,25 +13,29 @@
           scope="prototype"
           c:messageDecoder-ref="SoapDecoder" />
 
-    <bean id="SoapDecoder"
+    <bean id="SoapDecoder" 
           class="org.opensaml.saml.saml1.binding.decoding.impl.HTTPSOAP11Decoder"
           scope="prototype"
           p:httpServletRequest-ref="shibboleth.HttpServletRequest" />
 
     <bean id="ProcessSamlMessage"
-          class="net.shibboleth.idp.cas.flow.impl.ProcessSamlMessageAction" />
+          class="net.shibboleth.idp.cas.flow.impl.ProcessSamlMessageAction"
+          scope="prototype" />
 
-    <bean id="BuildSamlValidationSuccessMessage"
+    <bean id="BuildSamlValidationSuccessMessage" 
           class="net.shibboleth.idp.cas.flow.impl.BuildSamlValidationSuccessMessageAction"
+          scope="prototype"
           c:strategy-ref="shibboleth.DefaultIdentifierGenerationStrategy"
-          c:id="%{idp.entityID}" />
+          c:id="%{idp.entityID}"
+          p:outgoingBinding-ref="shibboleth.Binding.SAML1SOAP" />
 
     <bean id="BuildSamlValidationFailureMessage"
-          class="net.shibboleth.idp.cas.flow.impl.BuildSamlValidationFailureMessageAction" />
+          class="net.shibboleth.idp.cas.flow.impl.BuildSamlValidationFailureMessageAction"
+          scope="prototype"
+          p:outgoingBinding-ref="shibboleth.Binding.SAML1SOAP" />
 
     <bean id="MessageEncoderFactory"
-          class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory"
-          p:bindings-ref="shibboleth.OutgoingSOAPBindings" />
+          class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
 
     <bean id="EncodeMessage"
           class="org.opensaml.profile.action.impl.EncodeMessage"
diff --git a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml
index bc8bcc6..192691b 100644
--- a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml
@@ -31,7 +31,20 @@
     <util:constant id="shibboleth.MetadataLookup.Role"
         static-field="org.opensaml.saml.saml2.metadata.SPSSODescriptor.DEFAULT_ELEMENT_NAME" />
 
-    <alias name="shibboleth.OutgoingSAML2SLOBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+            class="net.shibboleth.ext.spring.util.SpringExpressionFunction"
+            c:expression="#custom[#input.isBrowserProfile() ? 'full' : 'soap']">
+        <property name="customObject">
+            <map>
+                <entry key="full">
+                    <ref bean="shibboleth.OutgoingSAML2SLOBindings" />
+                </entry>
+                <entry key="soap">
+                    <ref bean="shibboleth.OutgoingSOAPBindings" />
+                </entry>
+            </map>
+        </property>
+    </bean>    
     
     <bean id="PrepareInboundMessageContext"
         class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype" />
diff --git a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
index 1a2294a..56cf2af 100644
--- a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
@@ -101,16 +101,19 @@
     </end-state>
 
     <!--
-    Terminal state. This is "proceed" because the parent flow will respond to that
+    Terminal states. This is usually "proceed" because the parent flow will respond to that
     by emitting the outcome in the LogoutPropagationContext, which is set appropriately.
     -->
     <end-state id="proceed"/>
+    <end-state id="EndpointResolutionFailed" />
 
     <!--
-    Default is to turn non-proceed events into an error, which in this special flow is actually "proceed".
+    Catch-all is to turn non-proceed events into an error, which in this special flow is actually "proceed".
+    Some specifically interesting events are captured.
     -->
     <global-transitions>
         <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" />
+        <transition on="EndpointResolutionFailed" to="EndpointResolutionFailed" />
         <transition on="#{!'proceed'.equals(currentEvent.id)}" to="proceed" />
     </global-transitions>
     
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml
index 47d70c6..c3f4d92 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml
@@ -124,7 +124,7 @@
         class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
         p:endpointResolver-ref="shibboleth.EndpointResolver"
         p:endpointType="#{getObject('shibboleth.EndpointType')}"
-        p:bindings-ref="shibboleth.OutgoingBindings"
+        p:bindingDescriptorsLookupStrategy-ref="shibboleth.OutgoingBindingsLookupStrategy"
         p:artifactImpliesSecureChannel="%{idp.artifact.secureChannel:true}" />
         
     <bean id="PopulateDelegationContext"
@@ -321,8 +321,7 @@
         </property>
     </bean>
 
-    <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory"
-        p:bindings-ref="shibboleth.OutgoingBindings" />
+    <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
 
     <bean id="EncodeMessage" class="org.opensaml.profile.action.impl.EncodeMessage" scope="prototype"
         p:messageEncoderFactory-ref="messageEncoderFactory"
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml1/artifact-resolution-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml1/artifact-resolution-beans.xml
index ee4473a..e1768ba 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml1/artifact-resolution-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml1/artifact-resolution-beans.xml
@@ -36,7 +36,9 @@
         p:httpServletRequest-ref="shibboleth.HttpServletRequest"
         p:defaultPortInsecure="%{idp.security.defaultPortInsecure:true}" />
 
-    <alias name="shibboleth.OutgoingSOAPBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSOAPBindings" />
 
     <bean id="ResolveArtifacts"
         class="org.opensaml.saml.saml1.profile.impl.ResolveArtifacts" scope="prototype"
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml1/attribute-query-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml1/attribute-query-beans.xml
index f2d2ee7..1ec5849 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml1/attribute-query-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml1/attribute-query-beans.xml
@@ -37,7 +37,9 @@
         p:httpServletRequest-ref="shibboleth.HttpServletRequest"
         p:defaultPortInsecure="%{idp.security.defaultPortInsecure:true}" />
 
-    <alias name="shibboleth.OutgoingSOAPBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSOAPBindings" />
 
     <bean id="CopyNameIdentifierFromRequest"
             class="org.opensaml.saml.saml1.profile.impl.CopyNameIdentifierFromRequest" scope="prototype" />
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml1/common-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml1/common-beans.xml
index e53c4ff..763fcca 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml1/common-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml1/common-beans.xml
@@ -60,11 +60,8 @@
     </bean>
 
     <bean id="InitializeOutboundMessageContextForError"
-            class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContextForError" scope="prototype">
-        <property name="outboundBinding">
-            <util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML1_SOAP11_BINDING_URI"/>
-        </property>
-    </bean>
+            class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContextForError" scope="prototype"
+            p:outboundBinding-ref="shibboleth.Binding.SAML1SOAP" />
 
     <bean id="AddStatusToResponse"
             class="org.opensaml.saml.saml1.profile.impl.AddStatusToResponse" scope="prototype"
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml1/sso-unsolicited-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml1/sso-unsolicited-beans.xml
index 32135ec..b605d4d 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml1/sso-unsolicited-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml1/sso-unsolicited-beans.xml
@@ -20,7 +20,9 @@
                 p:bindingDescriptor-ref="shibboleth.Binding.LegacyShibboleth" />
         </constructor-arg>
     </bean>
-
-    <alias name="shibboleth.OutgoingSAML1SSOBindings" alias="shibboleth.OutgoingBindings"/>
+    
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML1SSOBindings" />
     
 </beans>
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/artifact-resolution-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/artifact-resolution-beans.xml
index 7b2b339..05e45f1 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/artifact-resolution-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/artifact-resolution-beans.xml
@@ -36,7 +36,9 @@
         p:httpServletRequest-ref="shibboleth.HttpServletRequest"
         p:defaultPortInsecure="%{idp.security.defaultPortInsecure:true}" />
 
-    <alias name="shibboleth.OutgoingSOAPBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSOAPBindings" />
 
     <bean id="AddArtifactResponseShell" class="org.opensaml.saml.saml2.profile.impl.AddStatusResponseShell" scope="prototype"
             p:issuerLookupStrategy-ref="shibboleth.ResponderIdLookup.Simple"
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/attribute-query-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/attribute-query-beans.xml
index 4d80acb..95779b9 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/attribute-query-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/attribute-query-beans.xml
@@ -37,7 +37,9 @@
         p:httpServletRequest-ref="shibboleth.HttpServletRequest"
         p:defaultPortInsecure="%{idp.security.defaultPortInsecure:true}" />
 
-    <alias name="shibboleth.OutgoingSOAPBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSOAPBindings" />
 
     <bean id="CopyNameIDFromRequest"
             class="org.opensaml.saml.saml2.profile.impl.CopyNameIDFromRequest" scope="prototype" />
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/common-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/common-beans.xml
index 8caa745..1c212ef 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/common-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/common-beans.xml
@@ -74,11 +74,8 @@
     </bean>
 
     <bean id="InitializeOutboundMessageContextForError"
-            class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContextForError" scope="prototype">
-        <property name="outboundBinding">
-            <util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML2_SOAP11_BINDING_URI"/>
-        </property>
-    </bean>
+            class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContextForError" scope="prototype"
+            p:outboundBinding-ref="shibboleth.Binding.SAML2SOAP" />
 
     <bean id="AddStatusToResponse"
             class="org.opensaml.saml.saml2.profile.impl.AddStatusToResponse" scope="prototype"
@@ -99,16 +96,6 @@
             <bean class="net.shibboleth.idp.profile.context.navigate.SpringStatusMessageLookupFunction" />
         </property>
     </bean>
-
-    <bean id="AddPartialLogoutStatusToResponse"
-            class="org.opensaml.saml.saml2.profile.impl.AddStatusToResponse" scope="prototype">
-        <property name="statusCodes">
-            <list>
-                <util:constant static-field="org.opensaml.saml.saml2.core.StatusCode.SUCCESS" />
-                <util:constant static-field="org.opensaml.saml.saml2.core.StatusCode.PARTIAL_LOGOUT" />
-            </list>
-        </property>
-    </bean>
     
     <bean id="EncryptToSelfParametersStrategy" class="org.opensaml.saml.security.impl.InlineSelfEncryptionParametersStrategy">
         <constructor-arg>
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/idwsf-ssos-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/idwsf-ssos-beans.xml
index f4ce453..3bc20a8 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/idwsf-ssos-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/idwsf-ssos-beans.xml
@@ -28,7 +28,9 @@
         </constructor-arg>
     </bean>
     
-    <alias name="shibboleth.OutgoingIDWSFSSOSBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingIDWSFSSOSBindings" />
     
     <bean id="shibboleth.BindingSpecificMessageHandler"
             class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-beans.xml
index f7e2e3e..90fa64d 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-beans.xml
@@ -23,6 +23,7 @@
     <bean id="InitializeProfileRequestContext"
         class="net.shibboleth.idp.profile.impl.InitializeProfileRequestContext" scope="prototype"
         p:profileId="#{T(net.shibboleth.idp.saml.saml2.profile.config.SingleLogoutProfileConfiguration).PROFILE_ID}"
+        p:browserProfile="false"
         p:loggingId="%{idp.service.logging.saml2slo:Logout}" />
 
     <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
@@ -34,28 +35,44 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSOAPBindings" />
+
     <bean id="InitializeMessageChannelSecurityContext"
         class="org.opensaml.profile.action.impl.HttpServletRequestMessageChannelSecurity" scope="prototype"
         p:httpServletRequest-ref="shibboleth.HttpServletRequest"
         p:defaultPortInsecure="%{idp.security.defaultPortInsecure:true}" />
 
-    <alias name="shibboleth.OutgoingSOAPBindings" alias="shibboleth.OutgoingBindings"/>
-
     <bean id="ProcessLogoutRequest"
         class="net.shibboleth.idp.saml.saml2.profile.impl.ProcessLogoutRequest" scope="prototype"
         p:sessionManager-ref="shibboleth.SessionManager"
         p:sessionResolver-ref="shibboleth.SessionManager" />
     
     <bean id="LogoutPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
-        p:fieldExtractors="#{getObject('shibboleth.LogoutAuditExtractors') ?: getObject('shibboleth.DefaultLogoutAuditExtractors')}" />
+        p:fieldExtractors="#{getObject('shibboleth.LogoutRequestAuditExtractors') ?: getObject('shibboleth.DefaultLogoutRequestAuditExtractors')}" />
+    
+    <bean id="SaveProfileRequestContextTree"
+        class="net.shibboleth.idp.session.impl.SaveProfileRequestContextTree" scope="prototype" />
+    
+    <bean id="RestoreProfileRequestContextTree"
+        class="net.shibboleth.idp.session.impl.RestoreProfileRequestContextTree" scope="prototype" />
+    
+    <bean id="PopulateLogoutPropagationContext"
+            class="net.shibboleth.idp.session.impl.PopulateLogoutPropagationContext" scope="prototype">
+        <property name="sessionLookupStrategy">
+            <bean class="com.google.common.base.Functions" factory-method="compose"
+                c:f-ref="shibboleth.ChildLookup.LogoutContext">
+                <constructor-arg name="g">
+                    <bean class="net.shibboleth.idp.session.context.navigate.LogoutContextSessionLookupFunction" />
+                </constructor-arg>    
+            </bean>
+        </property>
+    </bean>
     
-    <!--
-    TODO: will be used when logout propagation is enabled
-    <bean id="PopulateMultiRPContextFromLogoutContext"
-        class="net.shibboleth.idp.session.impl.PopulateMultiRPContextFromLogoutContext" scope="prototype"
-        p:roleDescriptorResolver-ref="shibboleth.RoleDescriptorResolver"
-        p:sessionTypeProtocolMap="#{getObject('shibboleth.SessionTypeProtocolMap') ?: getObject('shibboleth.DefaultSessionTypeProtocolMap')}" />
-    -->
+    <bean id="SelectPropagationFlow"
+        class="net.shibboleth.idp.session.impl.SelectLogoutPropagationFlow" scope="prototype"
+        c:selector-ref="shibboleth.LogoutPropagationFlowSelector" />
     
     <bean id="AddLogoutResponse" class="org.opensaml.saml.saml2.profile.impl.AddStatusResponseShell" scope="prototype"
             p:overwriteExisting="true"
@@ -67,11 +84,20 @@
         </property>
     </bean>
 
+    <bean id="AddPartialLogoutStatusToResponse"
+            class="org.opensaml.saml.saml2.profile.impl.AddStatusToResponse" scope="prototype">
+        <property name="statusCodes">
+            <list>
+                <util:constant static-field="org.opensaml.saml.saml2.core.StatusCode.SUCCESS" />
+                <util:constant static-field="org.opensaml.saml.saml2.core.StatusCode.PARTIAL_LOGOUT" />
+            </list>
+        </property>
+    </bean>
+
     <bean id="shibboleth.PreEncodeMessageHandler"
             class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
         <property name="handlers">
             <util:list>
-                <!-- TODO outbound handlers -->
                 <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler"
                     scope="prototype" p:signErrorResponses="%{idp.errors.signed:true}" />
             </util:list>
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-flow.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-flow.xml
index 9356e19..f30f92c 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-back-flow.xml
@@ -5,6 +5,10 @@
 
     <!-- Bootstrap action has to be at the "top" of the flow after this flow prepends itself to parent. -->
     
+    <on-start>
+        <set name="conversationScope.LogoutStatus" value="true" />
+    </on-start>
+    
     <action-state id="InitializeProfileRequestContext">
         <evaluate expression="InitializeProfileRequestContext" />
         <evaluate expression="'proceed'" />
@@ -15,16 +19,66 @@
     <action-state id="DoProfileWork">
         <evaluate expression="ProcessLogoutRequest" />
         <evaluate expression="LogoutPopulateAuditContext" />
-        <!--
-        TODO: will be used when logout propagation is enabled
-        <evaluate expression="PopulateMultiRPContextFromLogoutContext" />
-        -->
 
         <evaluate expression="'proceed'" />
         
-        <transition on="proceed" to="CheckAsync" />
+        <transition on="proceed" to="CheckForSessions" />
+    </action-state>
+
+    <decision-state id="CheckForSessions">
+        <if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutContext), true).getSessionMap().isEmpty()"
+            then="CheckAsync"
+            else="SaveProfileRequestContextTree" />
+    </decision-state>
+
+    <action-state id="SaveProfileRequestContextTree">
+        <evaluate expression="SaveProfileRequestContextTree" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PopulateNextLogoutPropagationContext" />
+    </action-state>
+
+    <action-state id="PopulateNextLogoutPropagationContext">
+        <evaluate expression="PopulateLogoutPropagationContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PropagateLogout" />
+        <transition on="SessionNotFound" to="RestoreProfileRequestContextTree" />
+    </action-state>
+    
+    <action-state id="PropagateLogout">
+        <evaluate expression="SelectPropagationFlow" />
+        
+        <!-- Call a subflow with the same ID as the event. -->
+        <transition on="#{currentEvent.id.startsWith('logoutprop/')}" to="CallPropagationFlow" />
+
+        <!-- Try the next RP's session(s), and record overall failure. -->
+        <transition to="PopulateNextLogoutPropagationContext">
+            <set name="conversationScope.LogoutStatus" value="false" />
+        </transition>
     </action-state>
 
+    <!-- This invokes a propagation flow. We check for failure but otherwise just keep iterating. -->
+    <subflow-state id="CallPropagationFlow" subflow="#{currentEvent.id}">
+        <input name="calledAsSubflow" value="true" />
+        <transition to="PopulateNextLogoutPropagationContext" />
+        <on-exit>
+            <evaluate expression="WriteAuditLog" />
+            <!-- If overall status remains positive, intersect with the last propagation result's success. -->
+            <evaluate expression="conversationScope.LogoutStatus and opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutPropagationContext)).getResult() == T(net.shibboleth.idp.session.context.LogoutPropagationContext.Result).Success"
+                result="conversationScope.LogoutStatus" />
+            <evaluate expression="opensamlProfileRequestContext.removeSubcontext(T(net.shibboleth.idp.session.context.LogoutPropagationContext))" />
+            <evaluate expression="opensamlProfileRequestContext.removeSubcontext(T(net.shibboleth.idp.profile.context.AuditContext))" />
+        </on-exit>
+    </subflow-state>
+    
+    <action-state id="RestoreProfileRequestContextTree">
+        <evaluate expression="RestoreProfileRequestContextTree" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckAsync" />    
+    </action-state>
+    
     <decision-state id="CheckAsync">
         <if test="opensamlProfileRequestContext.getInboundMessageContext().getMessage().getExtensions() == null or opensamlProfileRequestContext.getInboundMessageContext().getMessage().getExtensions().getUnknownXMLObjects(T(org.opensaml.saml.ext.saml2aslo.Asynchronous).DEFAULT_ELEMENT_NAME).isEmpty()"
             then="BuildResponse" else="DoAsync" />
@@ -57,10 +111,8 @@
         <transition on="proceed" to="CheckForPartialLogout" />
     </action-state>
     
-    <!-- TODO: this partial logout machinery will change once propagation exists -->
-    
     <decision-state id="CheckForPartialLogout">
-        <if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutContext), true).getSessionMap().isEmpty()"
+        <if test="conversationScope.LogoutStatus"
             then="CompleteResponse" else="PartialLogout" />
     </decision-state>
     
@@ -86,6 +138,7 @@
         <on-entry>
             <evaluate expression="LogEvent" />
         </on-entry>
+        <evaluate expression="RestoreProfileRequestContextTree" />
         <evaluate expression="InitializeOutboundMessageContextForError" />
         <evaluate expression="AddLogoutResponse" />
         <evaluate expression="AddInResponseToToResponse" />
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml
index e789a2d..2c1a453 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml
@@ -30,7 +30,9 @@
         class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
         p:confidentialityActive="false" p:integrityActive="false" />
 
-    <alias name="shibboleth.OutgoingSAML2SLOFrontBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML2SLOFrontBindings" />    
 
     <bean id="ProcessLogoutRequest"
         class="net.shibboleth.idp.saml.saml2.profile.impl.ProcessLogoutRequest" scope="prototype"
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-artifact-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-artifact-beans.xml
index 74443d5..74d2529 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-artifact-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-artifact-beans.xml
@@ -37,6 +37,8 @@
         </constructor-arg>
     </bean>
     
-    <alias name="shibboleth.OutgoingSAML2SSOBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML2SSOBindings" />    
     
 </beans>
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-ecp-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-ecp-beans.xml
index 85f2264..96651d4 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-ecp-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-ecp-beans.xml
@@ -27,8 +27,10 @@
                 p:bindingDescriptor-ref="shibboleth.Binding.SAML2SOAP" />
         </constructor-arg>
     </bean>
-    
-    <alias name="shibboleth.OutgoingECPBindings" alias="shibboleth.OutgoingBindings"/>
+
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingECPBindings" />    
     
     <bean id="shibboleth.BindingSpecificMessageHandler"
             class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-beans.xml
index 186f681..4668288 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-beans.xml
@@ -28,6 +28,8 @@
         </constructor-arg>
     </bean>
     
-    <alias name="shibboleth.OutgoingSAML2SSOBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML2SSOBindings" />    
     
 </beans>
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-simplesign-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-simplesign-beans.xml
index 041675f..a964102 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-simplesign-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-post-simplesign-beans.xml
@@ -29,6 +29,8 @@
         </constructor-arg>
     </bean>
     
-    <alias name="shibboleth.OutgoingSAML2SSOBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML2SSOBindings" />    
     
 </beans>
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-redirect-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-redirect-beans.xml
index 7a75dc1..7d3988e 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-redirect-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-redirect-beans.xml
@@ -28,6 +28,8 @@
         </constructor-arg>
     </bean>
 
-    <alias name="shibboleth.OutgoingSAML2SSOBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML2SSOBindings" />    
 
 </beans>
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-unsolicited-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-unsolicited-beans.xml
index d618431..5328828 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/sso-unsolicited-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/sso-unsolicited-beans.xml
@@ -27,6 +27,8 @@
         </constructor-arg>
     </bean>
 
-    <alias name="shibboleth.OutgoingSAML2SSOBindings" alias="shibboleth.OutgoingBindings"/>
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML2SSOBindings" />    
 
 </beans>
\ No newline at end of file
diff --git a/idp-conf/src/test/resources/metadata/example-sp123-metadata.xml b/idp-conf/src/test/resources/metadata/example-sp123-metadata.xml
index d0f6b64..b2e75b2 100644
--- a/idp-conf/src/test/resources/metadata/example-sp123-metadata.xml
+++ b/idp-conf/src/test/resources/metadata/example-sp123-metadata.xml
@@ -23,6 +23,10 @@ Example metadata for mock SPs with entityIDs of form 'https://sp[123].example.or
 	            <saml:AttributeValue>http://refeds.org/category/research-and-scholarship
 	            </saml:AttributeValue>
 	        </saml:Attribute>
+            <saml:Attribute Name="http://shibboleth.net/ns/profiles/saml2/logout/signSOAPRequests"
+                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+                <saml:AttributeValue>true</saml:AttributeValue>
+            </saml:Attribute>
 	    </mdattr:EntityAttributes>
     </Extensions>
     <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol" AuthnRequestsSigned="false">
@@ -91,6 +95,7 @@ h+XNZ5oAql/cjrwDWw==
             </KeyInfo>
         </KeyDescriptor>
         
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://localhost:8443/sp/sp1/SAML2/SOAP/SLO"/>
         <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/sp/sp1/SAML2/Redirect/SLO"/>
         
         <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/sp/sp1/SAML2/POST/ACS" index="1"/>
@@ -202,6 +207,7 @@ h+XNZ5oAql/cjrwDWw==
             </KeyInfo>
         </KeyDescriptor>
         
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://localhost:8443/sp/sp2/SAML2/SOAP/SLO"/>
         <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/sp/sp2/SAML2/Redirect/SLO"/>
         
         <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/sp/sp2/SAML2/POST/ACS" index="1"/>
@@ -313,6 +319,7 @@ h+XNZ5oAql/cjrwDWw==
             </KeyInfo>
         </KeyDescriptor>
         
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://localhost:8443/sp/sp3/SAML2/SOAP/SLO"/>
         <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/sp/sp3/SAML2/Redirect/SLO"/>
         
         <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/sp/sp3/SAML2/POST/ACS" index="1"/>
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForError.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForError.java
index d67eb42..681e060 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForError.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForError.java
@@ -27,7 +27,6 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 import org.opensaml.messaging.context.BaseContext;
 import org.opensaml.messaging.context.MessageContext;
@@ -36,6 +35,7 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.opensaml.saml.common.binding.BindingDescriptor;
 import org.opensaml.saml.common.binding.SAMLBindingSupport;
 import org.opensaml.saml.common.messaging.context.SAMLBindingContext;
 import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
@@ -73,7 +73,7 @@ public class InitializeOutboundMessageContextForError extends AbstractProfileAct
     @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
     
     /** Outbound binding to use. */
-    @NonnullAfterInit private String outboundBinding;
+    @NonnullAfterInit private BindingDescriptor outboundBinding;
     
     /** The {@link SAMLPeerEntityContext} to base the outbound context on, if any. */
     @Nullable private SAMLPeerEntityContext peerEntityCtx;
@@ -115,11 +115,10 @@ public class InitializeOutboundMessageContextForError extends AbstractProfileAct
     /**
      * Set the outbound binding to use for the error response.
      * 
-     * @param binding   binding identifier
+     * @param binding   binding descriptor
      */
-    public void setOutboundBinding(@Nonnull @NotEmpty final String binding) {
-        outboundBinding = Constraint.isNotNull(StringSupport.trimOrNull(binding),
-                "Outbound binding URI cannot be null or empty");
+    public void setOutboundBinding(@Nonnull @NotEmpty final BindingDescriptor binding) {
+        outboundBinding = Constraint.isNotNull(binding, "Outbound BindingDescriptor cannot be null or empty");
     }
     
     /** {@inheritDoc} */
@@ -128,7 +127,7 @@ public class InitializeOutboundMessageContextForError extends AbstractProfileAct
         super.doInitialize();
         
         if (outboundBinding == null) {
-            throw new ComponentInitializationException("Outbound binding URI cannot be null");
+            throw new ComponentInitializationException("Outbound BindingDescriptor cannot be null");
         }
     }
 
@@ -172,7 +171,7 @@ public class InitializeOutboundMessageContextForError extends AbstractProfileAct
             bindingCtx.setRelayState(SAMLBindingSupport.getRelayState(
                     profileRequestContext.getInboundMessageContext()));
         }
-        bindingCtx.setBindingUri(outboundBinding);
+        bindingCtx.setBindingDescriptor(outboundBinding);
                 
         // Copy SAML peer context and metadata if it exists.
         if (peerEntityCtx != null) {
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContexts.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContexts.java
index 1d6ca7b..9e67888 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContexts.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContexts.java
@@ -37,6 +37,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
@@ -70,8 +71,6 @@ import com.google.common.base.Function;
 import com.google.common.base.Functions;
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.Iterables;
 
 /**
@@ -107,9 +106,9 @@ public class PopulateBindingAndEndpointContexts extends AbstractProfileAction {
     /** Endpoint resolver. */
     @NonnullAfterInit private EndpointResolver<?> endpointResolver;
     
-    /** List of possible bindings, in preference order. */
-    @Nonnull @NonnullElements private List<BindingDescriptor> bindingDescriptors;
-
+    /** Lookup strategy for bindings. */
+    @Nonnull private Function<ProfileRequestContext,List<BindingDescriptor>> bindingDescriptorsLookupStrategy;
+    
     /** Strategy function for access to {@link RelyingPartyContext}. */
     @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
     
@@ -125,6 +124,9 @@ public class PopulateBindingAndEndpointContexts extends AbstractProfileAction {
     /** Strategy function for access to {@link SAMLArtifactContext} to populate. */
     @Nonnull private Function<ProfileRequestContext,SAMLArtifactContext> artifactContextLookupStrategy;
     
+    /** List of possible bindings, in preference order. */
+    @Nonnull @NonnullElements private List<BindingDescriptor> bindingDescriptors;
+    
     /** Whether an artifact-based binding implies the use of a secure channel. */
     private boolean artifactImpliesSecureChannel;
     
@@ -151,7 +153,9 @@ public class PopulateBindingAndEndpointContexts extends AbstractProfileAction {
     
     /** Constructor. */
     public PopulateBindingAndEndpointContexts() {
-        bindingDescriptors = Collections.emptyList();
+        bindingDescriptorsLookupStrategy =
+                FunctionSupport.<ProfileRequestContext,List<BindingDescriptor>>constant(
+                        Collections.<BindingDescriptor>emptyList());
         
         relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
         
@@ -201,17 +205,20 @@ public class PopulateBindingAndEndpointContexts extends AbstractProfileAction {
     }
     
     /**
-     * Set the bindings to evaluate for use, in preference order.
+     * Set lookup strategy to return the bindings to evaluate for use, in preference order.
+     * 
+     * @param strategy lookup strategy
      * 
-     * @param bindings bindings to consider
+     * @since 4.0.0
      */
-    public void setBindings(@Nonnull @NonnullElements final List<BindingDescriptor> bindings) {
+    public void setBindingDescriptorsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,List<BindingDescriptor>> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        Constraint.isNotNull(bindings, "Binding descriptor list cannot be null");
         
-        bindingDescriptors = new ArrayList<>(Collections2.filter(bindings, Predicates.notNull()));
+        bindingDescriptorsLookupStrategy =
+                Constraint.isNotNull(strategy, "Binding descriptors lookup strategy cannot be null");
     }
-
+    
     /**
      * Set lookup strategy for {@link RelyingPartyContext}.
      * 
@@ -355,8 +362,14 @@ public class PopulateBindingAndEndpointContexts extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         }
+       
+        bindingDescriptors = bindingDescriptorsLookupStrategy.apply(profileRequestContext);
+        if (bindingDescriptors == null) {
+            bindingDescriptors = Collections.emptyList();
+        }
         
         mdContext = metadataContextLookupStrategy.apply(profileRequestContext);
+        
         return true;
     }
 
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java
index 4f03706..8282a53 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java
@@ -17,18 +17,14 @@
 
 package net.shibboleth.idp.saml.profile.impl;
 
-import java.util.List;
-
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.shibboleth.idp.profile.context.SpringRequestContext;
 import net.shibboleth.idp.saml.binding.BindingDescriptor;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
 
 import org.opensaml.messaging.encoder.MessageEncoder;
 import org.opensaml.profile.action.MessageEncoderFactory;
@@ -38,12 +34,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeansException;
 
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ListMultimap;
 
 /**
- * A factory that returns the correct {@link MessageEncoder} to use based on an input collection of
- * descriptors that map to a Spring bean ID.
+ * A factory that returns the correct {@link MessageEncoder} to use based on an underlying {@link BindingDescriptor}.
  */
 @ThreadSafeAfterInit
 public class SpringAwareMessageEncoderFactory extends AbstractInitializableComponent implements MessageEncoderFactory {
@@ -51,31 +44,6 @@ public class SpringAwareMessageEncoderFactory extends AbstractInitializableCompo
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(SpringAwareMessageEncoderFactory.class);
     
-    /** Map of bindings to descriptors. */
-    @Nonnull @NonnullElements private ListMultimap<String,BindingDescriptor> bindingMap;
-    
-    /** Constructor. */
-    public SpringAwareMessageEncoderFactory() {
-        bindingMap = ArrayListMultimap.create();
-    }
-    
-    /**
-     * Set the bindings to evaluate for use, in preference order.
-     * 
-     * @param bindings bindings to consider
-     */
-    public void setBindings(@Nonnull @NonnullElements final List<BindingDescriptor> bindings) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        Constraint.isNotNull(bindings, "Binding descriptor list cannot be null");
-        
-        bindingMap.clear();
-        for (final BindingDescriptor binding : bindings) {
-            if (binding != null && binding.getId() != null) {
-                bindingMap.put(binding.getId(), binding);
-            }
-        }
-    }
-    
     /** {@inheritDoc} */
     @Override
     @Nullable public MessageEncoder getMessageEncoder(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -83,7 +51,7 @@ public class SpringAwareMessageEncoderFactory extends AbstractInitializableCompo
         
         final SpringRequestContext springContext = profileRequestContext.getSubcontext(SpringRequestContext.class);
         if (springContext == null || springContext.getRequestContext() == null) {
-            log.warn("No outbound message context, unable to lookup message encoder");
+            log.warn("No Spring request context, unable to lookup message encoder bean");
             return null;
         } else if (profileRequestContext.getOutboundMessageContext() == null) {
             log.warn("No outbound message context, unable to lookup message encoder");
@@ -92,22 +60,21 @@ public class SpringAwareMessageEncoderFactory extends AbstractInitializableCompo
         
         final SAMLBindingContext bindingContext =
                 profileRequestContext.getOutboundMessageContext().getSubcontext(SAMLBindingContext.class);
-        if (bindingContext == null || bindingContext.getBindingUri() == null) {
-            log.warn("Binding URI was not available, unable to lookup message encoder");
+        if (bindingContext == null || bindingContext.getBindingDescriptor() == null
+                || !(bindingContext.getBindingDescriptor() instanceof BindingDescriptor)) {
+            log.warn("BindingDescriptor was not available, unable to lookup message encoder");
             return null;
         }
         
         log.debug("Looking up message encoder based on binding URI: {}", bindingContext.getBindingUri());
-    
-        final List<BindingDescriptor> bindings = bindingMap.get(bindingContext.getBindingUri());
-        for (final BindingDescriptor binding : bindings) {
-            if (binding.getEncoderBeanId() != null) {
-                try {
-                    return springContext.getRequestContext().getActiveFlow().getApplicationContext().getBean(
-                            binding.getEncoderBeanId(), MessageEncoder.class);
-                } catch (final BeansException e) {
-                    log.warn("Error instantiating message encoder from bean ID {}", binding.getEncoderBeanId(), e);
-                }
+        
+        final BindingDescriptor descriptor = (BindingDescriptor) bindingContext.getBindingDescriptor();
+        if (descriptor.getEncoderBeanId() != null) {
+            try {
+                return springContext.getRequestContext().getActiveFlow().getApplicationContext().getBean(
+                        descriptor.getEncoderBeanId(), MessageEncoder.class);
+            } catch (final BeansException e) {
+                log.warn("Error instantiating message encoder from bean ID {}", descriptor.getEncoderBeanId(), e);
             }
         }
         
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SOAPLogoutRequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SOAPLogoutRequest.java
index 8f1defc..fb7770f 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SOAPLogoutRequest.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SOAPLogoutRequest.java
@@ -95,7 +95,7 @@ public class SOAPLogoutRequest extends AbstractProfileAction {
     /** Strategy function for access to {@link SAMLMetadataContext} for input to SOAP client. */
     @Nonnull private Function<ProfileRequestContext,SAMLMetadataContext> metadataContextLookupStrategy;
 
-    /** Strategy function for access to {@link SAMLEndpointContext} to populate. */
+    /** Strategy function for access to {@link SAMLEndpointContext} to retrieve address from. */
     @Nonnull private Function<ProfileRequestContext,SAMLEndpointContext> endpointContextLookupStrategy;
     
     /** SOAP client. */
@@ -309,11 +309,16 @@ public class SOAPLogoutRequest extends AbstractProfileAction {
             @Nonnull final LogoutResponse response) {
         final Status status = response.getStatus();
         if (status != null) {
-            final StatusCode code = status.getStatusCode();
+            StatusCode code = status.getStatusCode();
             if (code != null) {
                 if (StatusCode.SUCCESS.equals(code.getValue())) {
-                    log.debug("{} LogoutResponse was successful", getLogPrefix());
-                    propagationContext.setResult(Result.Success);
+                    code = code.getStatusCode();
+                    if (code == null || code.getValue() == null || !StatusCode.PARTIAL_LOGOUT.equals(code.getValue())) {
+                        log.debug("{} Logout successful", getLogPrefix());
+                        propagationContext.setResult(Result.Success);
+                    } else {
+                        log.debug("{} Logout partially successful", getLogPrefix());
+                    }
                     return;
                 }
                 log.warn("{} LogoutResponse received with status code '{}'", getLogPrefix(), code.getValue());
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java
index 18318f4..34cfca8 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java
@@ -94,7 +94,7 @@ public class PrepareInboundMessageContext extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         } else if (logoutPropCtx.getSession() == null || !(logoutPropCtx.getSession() instanceof SAML2SPSession)) {
-            log.debug("{} Logout propgation context did not contain a SAML2SPSession", getLogPrefix());
+            log.debug("{} Logout propagation context did not contain a SAML2SPSession", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForErrorTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForErrorTest.java
index 30c6615..566555a 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForErrorTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/InitializeOutboundMessageContextForErrorTest.java
@@ -21,6 +21,7 @@ import net.shibboleth.idp.profile.ActionTestingSupport;
 import net.shibboleth.idp.profile.RequestContextBuilder;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.saml.binding.BindingDescriptor;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
 import org.opensaml.core.OpenSAMLInitBaseTestCase;
@@ -55,7 +56,10 @@ public class InitializeOutboundMessageContextForErrorTest extends OpenSAMLInitBa
         prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
         prc.setOutboundMessageContext(null);
         action = new InitializeOutboundMessageContextForError();
-        action.setOutboundBinding(SAMLConstants.SAML1_SOAP11_BINDING_URI);
+        final BindingDescriptor bd = new BindingDescriptor();
+        bd.setId(SAMLConstants.SAML1_SOAP11_BINDING_URI);
+        bd.initialize();
+        action.setOutboundBinding(bd);
         action.initialize();
     }
     
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContextsTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContextsTest.java
index 9438dbc..1bd4508 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContextsTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/profile/impl/PopulateBindingAndEndpointContextsTest.java
@@ -36,6 +36,7 @@ import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileR
 import net.shibboleth.idp.saml.saml2.profile.config.BrowserSSOProfileConfiguration;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 import net.shibboleth.utilities.java.support.xml.XMLParserException;
 
 import org.opensaml.core.xml.XMLObjectBaseTestCase;
@@ -105,7 +106,7 @@ public class PopulateBindingAndEndpointContextsTest extends XMLObjectBaseTestCas
         bindings.add(new BindingDescriptor());
         bindings.get(0).setId(SAMLConstants.SAML2_POST_BINDING_URI);
         bindings.get(0).initialize();
-        action.setBindings(bindings);
+        action.setBindingDescriptorsLookupStrategy(FunctionSupport.<ProfileRequestContext,List<BindingDescriptor>>constant(bindings));
         action.initialize();
     }
 
@@ -138,7 +139,8 @@ public class PopulateBindingAndEndpointContextsTest extends XMLObjectBaseTestCas
         binding.initialize();
         final PopulateBindingAndEndpointContexts badaction = new PopulateBindingAndEndpointContexts();
         badaction.setEndpointResolver(new DefaultEndpointResolver());
-        badaction.setBindings(Collections.singletonList(binding));
+        badaction.setBindingDescriptorsLookupStrategy(
+                FunctionSupport.<ProfileRequestContext,List<BindingDescriptor>>constant(Collections.singletonList(binding)));
         badaction.initialize();
         
         final Event event = badaction.execute(rc);
@@ -287,7 +289,8 @@ public class PopulateBindingAndEndpointContextsTest extends XMLObjectBaseTestCas
         binding.initialize();
         final PopulateBindingAndEndpointContexts badaction = new PopulateBindingAndEndpointContexts();
         badaction.setEndpointResolver(new DefaultEndpointResolver());
-        badaction.setBindings(Collections.singletonList(binding));
+        badaction.setBindingDescriptorsLookupStrategy(
+                FunctionSupport.<ProfileRequestContext,List<BindingDescriptor>>constant(Collections.singletonList(binding)));
         badaction.initialize();
         
         final Event event = badaction.execute(rc);
diff --git a/idp-session-api/src/main/java/net/shibboleth/idp/session/context/navigate/LogoutContextSessionLookupFunction.java b/idp-session-api/src/main/java/net/shibboleth/idp/session/context/navigate/LogoutContextSessionLookupFunction.java
new file mode 100644
index 0000000..7809247
--- /dev/null
+++ b/idp-session-api/src/main/java/net/shibboleth/idp/session/context/navigate/LogoutContextSessionLookupFunction.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.session.context.navigate;
+
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.idp.session.context.LogoutContext;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+
+/**
+ * A function that returns a session from a {@link LogoutContext}
+ * and removes it from that context at the same time.
+ * 
+ * <p>The exact session returned is unspecified.</p>
+ */
+public class LogoutContextSessionLookupFunction implements ContextDataLookupFunction<LogoutContext,SPSession> {
+    
+    /** {@inheritDoc} */
+    @Nullable public SPSession apply(@Nullable final LogoutContext input) {
+        
+        if (input != null) {
+            final Collection<SPSession> sessions = input.getSessionMap().values();
+            if (sessions != null && !sessions.isEmpty()) {
+                final SPSession session = sessions.iterator().next();
+                if (session != null) {
+                    sessions.remove(session);
+                    return session;
+                }
+            }
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/PopulateLogoutPropagationContext.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/PopulateLogoutPropagationContext.java
index 135d1ba..b427b10 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/PopulateLogoutPropagationContext.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/PopulateLogoutPropagationContext.java
@@ -27,9 +27,8 @@ import net.shibboleth.idp.session.SPSession;
 import net.shibboleth.idp.session.SPSessionSerializerRegistry;
 import net.shibboleth.idp.session.context.LogoutContext;
 import net.shibboleth.idp.session.context.LogoutPropagationContext;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.idp.session.context.LogoutPropagationContext.Result;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.security.DataSealer;
@@ -49,14 +48,16 @@ import com.google.common.base.Function;
 import org.springframework.webflow.execution.RequestContext;
 
 /**
- * Profile action that creates a {@link LogoutPropagationContext} containing an {@link SPSession} to be destroyed. The
- * SP session may be populated by reference or value.
+ * Profile action that creates a {@link LogoutPropagationContext} containing {@link SPSession} to be destroyed. The
+ * SP sessions may be populated:
  * <ul>
- *     <li>By reference - via <em>SessionKey</em> request parameter that looks up {@link SPSession} from a
+ *     <li>By reference - via <em>SessionKey</em> request parameter that looks up one {@link SPSession} from a
  *     {@link LogoutContext} stored in the HTTP session.</li>
  *     <li>By value - reconstitutes an encrypted {@link SPSession} object in <em>SPSession</em> request parameter.</li>
+ *     <li>By lookup strategy.</li>
  * </ul>
  *
+ * @event SessionNotFound
  * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
  * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
  * @event {@link org.opensaml.profile.action.EventIds#UNABLE_TO_DECODE}
@@ -65,6 +66,9 @@ import org.springframework.webflow.execution.RequestContext;
 public class PopulateLogoutPropagationContext extends AbstractProfileAction {
 
     /** Name of parameter containing session by reference. */
+    @Nonnull @NotEmpty private static final String SESSION_NOT_FOUND = "SessionNotFound";
+
+    /** Name of parameter containing session by reference. */
     @Nonnull @NotEmpty private static final String SESSION_PARAM_BYREF = "SessionKey";
 
     /** Name of parameter containing session by value. */
@@ -77,10 +81,13 @@ public class PopulateLogoutPropagationContext extends AbstractProfileAction {
     @Nullable private DataSealer dataSealer;
     
     /** Mappings between a SPSession type and a serializer implementation. */
-    @NonnullAfterInit private SPSessionSerializerRegistry spSessionSerializerRegistry;
+    @Nullable private SPSessionSerializerRegistry spSessionSerializerRegistry;
+    
+    /** Lookup/creation function for LogoutPropagationContext. */
+    @Nonnull private Function<ProfileRequestContext,LogoutPropagationContext> contextCreationStrategy;
     
-    /** Creation function for LogoutPropagationContext. */
-    @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> contextCreationStrategy;
+    /** Lookup strategy for session. */
+    @Nullable private Function<ProfileRequestContext,SPSession> sessionLookupStrategy;
     
     /** {@link SPSession} to operate on. */
     @Nullable private SPSession session;
@@ -110,10 +117,10 @@ public class PopulateLogoutPropagationContext extends AbstractProfileAction {
      * 
      * @param registry a registry of SPSession class to serializer mappings
      */
-    public void setSPSessionSerializerRegistry(@Nonnull final SPSessionSerializerRegistry registry) {
+    public void setSPSessionSerializerRegistry(@Nullable final SPSessionSerializerRegistry registry) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
-        spSessionSerializerRegistry = Constraint.isNotNull(registry, "Registry cannot be null");
+        spSessionSerializerRegistry = registry;
     }
     
     /**
@@ -129,16 +136,18 @@ public class PopulateLogoutPropagationContext extends AbstractProfileAction {
                 "LogoutPropagationContext creation strategy cannot be null");
     }
     
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
+    /**
+     * Set a lookup strategy to use to obtain the session to populate.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSessionLookupStrategy(@Nullable final Function<ProfileRequestContext,SPSession> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        if (spSessionSerializerRegistry == null) {
-            throw new ComponentInitializationException("SPSessionSerializerRegistry cannot be null");
-        }
+        sessionLookupStrategy = strategy;
     }
-
+    
+// Checkstyle: ReturnCount|CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -146,6 +155,18 @@ public class PopulateLogoutPropagationContext extends AbstractProfileAction {
         if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
+        
+        if (sessionLookupStrategy != null) {
+            session = sessionLookupStrategy.apply(profileRequestContext);
+            if (session != null) {
+                log.debug("{} Got session to propagate logout: {}", getLogPrefix(), session);
+                return true;
+            } else {
+                log.debug("{} No sessions remaining for logout propagation", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, SESSION_NOT_FOUND);
+                return false;
+            }
+        }
 
         final RequestContext requestContext = getRequestContext(profileRequestContext);
         if (requestContext == null) {
@@ -161,10 +182,11 @@ public class PopulateLogoutPropagationContext extends AbstractProfileAction {
                 sessionKey = sessionRef;
                 session = getSessionByReference(requestContext, sessionKey);
             } else if (sessionVal != null) {
-                if (dataSealer != null) {
+                if (dataSealer != null && spSessionSerializerRegistry != null) {
                     session = getSessionByValue(sessionVal);
                 } else {
-                    log.error("{} No DataSealer provided, unable to decrypt session passed by value", getLogPrefix());
+                    log.error("{} No DataSealer/SerializerRegistry provided, unable to process session passed by value",
+                            getLogPrefix());
                     ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
                     return false;
                 }
@@ -186,18 +208,21 @@ public class PopulateLogoutPropagationContext extends AbstractProfileAction {
 
         return true;
     }
+// Checkstyle: ReturnCount|CyclomaticComplexity ON
 
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         final LogoutPropagationContext logoutPropCtx = contextCreationStrategy.apply(profileRequestContext);
         if (logoutPropCtx == null) {
-            log.error("{} Unable to create or locate SingleLogoutContext", getLogPrefix());
+            log.error("{} Unable to create or locate LogoutPropagationContext", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
         logoutPropCtx.setSession(session);
         logoutPropCtx.setSessionKey(sessionKey);
+        logoutPropCtx.setResult(Result.Failure);
+        logoutPropCtx.setDetail(null);
     }
 
     /**
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/RestoreProfileRequestContextTree.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/RestoreProfileRequestContextTree.java
new file mode 100644
index 0000000..b5d2f31
--- /dev/null
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/RestoreProfileRequestContextTree.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.session.impl;
+
+import com.google.common.base.Function;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.ActionSupport;
+import net.shibboleth.idp.profile.context.AuditContext;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.session.context.LogoutContext;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Restores specific portions of the context tree used during logout processing to enable
+ * reuse of logout propagation subflows during back channel logout.
+ * 
+ * <p>Some propagation flows make use of substantial portions of the tree, including the
+ * inbound/outbound contexts, audit context, relying party context, etc., so this amounts to
+ * a "push/pop" to preserve the state of the "outer" logout operation.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * 
+ * @pre LogoutContext.getSubcontext(InOutOperationContext.class) != null
+ * @post LogoutContext.getSubcontext(InOutOperationContext.class) == null
+ */
+public class RestoreProfileRequestContextTree extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RestoreProfileRequestContextTree.class);
+
+    /** Looks up a LogoutContext from PRC. */
+    @Nonnull private Function<ProfileRequestContext,LogoutContext> logoutContextLookupStrategy;
+            
+    /** Constructor. */
+    public RestoreProfileRequestContextTree() {
+        logoutContextLookupStrategy = new ChildContextLookup<>(LogoutContext.class);
+    }
+    
+    /**
+     * Set the lookup strategy for the {@link LogoutContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutContextLookupStrategy(@Nonnull final Function<ProfileRequestContext,LogoutContext> strategy) {
+        logoutContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final LogoutContext logoutContext = logoutContextLookupStrategy.apply(profileRequestContext);
+        if (logoutContext == null) {
+            log.debug("{} LogoutContext not found in ProfileRequestContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+
+        final InOutOperationContext placeholder = logoutContext.getSubcontext(InOutOperationContext.class);
+        if (placeholder == null) {
+            log.debug("{} InOutOperationContext not found under LogoutContext, nothing to do", getLogPrefix());
+            return;
+        }
+        
+        logoutContext.removeSubcontext(placeholder);
+
+        profileRequestContext.setInboundMessageContext(placeholder.getInboundMessageContext());
+        profileRequestContext.setOutboundMessageContext(placeholder.getOutboundMessageContext());
+        
+        final RelyingPartyContext relyingPartyCtx = placeholder.getSubcontext(RelyingPartyContext.class);
+        if (relyingPartyCtx != null) {
+            profileRequestContext.addSubcontext(relyingPartyCtx, true);
+        }
+        
+        final AuditContext auditCtx = placeholder.getSubcontext(AuditContext.class);
+        if (auditCtx != null) {
+            profileRequestContext.addSubcontext(auditCtx, true);
+        }
+        
+        log.debug("{} Restored LogoutRequest processing state after logout propagation", getLogPrefix());
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/SaveProfileRequestContextTree.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/SaveProfileRequestContextTree.java
new file mode 100644
index 0000000..a84b49d
--- /dev/null
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/SaveProfileRequestContextTree.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.session.impl;
+
+import com.google.common.base.Function;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.ActionSupport;
+import net.shibboleth.idp.profile.context.AuditContext;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.session.context.LogoutContext;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Saves off specific portions of the context tree in use during logout processing to enable
+ * reuse of logout propagation subflows during back channel logout.
+ * 
+ * <p>Some propagation flows make use of substantial portions of the tree, including the
+ * inbound/outbound contexts, audit context, relying party context, etc., so this amounts to
+ * a "push/pop" to preserve the state of the "outer" logout operation.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * 
+ * @post LogoutContext.getSubcontext(InOutOperationContext.class) != null
+ */
+public class SaveProfileRequestContextTree extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SaveProfileRequestContextTree.class);
+
+    /** Looks up a LogoutContext from PRC. */
+    @Nonnull private Function<ProfileRequestContext,LogoutContext> logoutContextLookupStrategy;
+            
+    /** Constructor. */
+    public SaveProfileRequestContextTree() {
+        logoutContextLookupStrategy = new ChildContextLookup<>(LogoutContext.class);
+    }
+    
+    /**
+     * Set the lookup strategy for the {@link LogoutContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutContextLookupStrategy(@Nonnull final Function<ProfileRequestContext,LogoutContext> strategy) {
+        logoutContextLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final LogoutContext logoutContext = logoutContextLookupStrategy.apply(profileRequestContext);
+        if (logoutContext == null) {
+            log.debug("{} LogoutContext not found in ProfileRequestContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        
+        final MessageContext inbound = profileRequestContext.getInboundMessageContext();
+        final MessageContext outbound = profileRequestContext.getOutboundMessageContext();
+        
+        final InOutOperationContext placeholder = new InOutOperationContext(inbound, outbound);
+        logoutContext.addSubcontext(placeholder);
+        profileRequestContext.setInboundMessageContext(null);
+        profileRequestContext.setOutboundMessageContext(null);
+        
+        final RelyingPartyContext relyingPartyCtx = profileRequestContext.getSubcontext(RelyingPartyContext.class);
+        if (relyingPartyCtx != null) {
+            placeholder.addSubcontext(relyingPartyCtx);
+        }
+        
+        final AuditContext auditCtx = profileRequestContext.getSubcontext(AuditContext.class);
+        if (auditCtx != null) {
+            placeholder.addSubcontext(auditCtx);
+        }
+        
+        log.debug("{} Saved off LogoutRequest processing state during logout propagation", getLogPrefix());
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/SelectLogoutPropagationFlow.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/SelectLogoutPropagationFlow.java
index 4504460..e77ed54 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/SelectLogoutPropagationFlow.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/SelectLogoutPropagationFlow.java
@@ -28,7 +28,6 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,7 +54,7 @@ public class SelectLogoutPropagationFlow extends AbstractProfileAction {
     @Nonnull private final Logger log = LoggerFactory.getLogger(SelectLogoutPropagationFlow.class);
 
     /** Selection function to determine suitable LogoutPropagationFlowDescriptor for given SPSession. */
-    @Nonnull private final Function<SPSession, LogoutPropagationFlowDescriptor> flowSelectorFunction;
+    @Nonnull private final Function<SPSession,LogoutPropagationFlowDescriptor> flowSelectorFunction;
 
     /** Function to retrieve LogoutPropagationContext from context tree. */
     @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextFunction;
@@ -80,20 +79,19 @@ public class SelectLogoutPropagationFlow extends AbstractProfileAction {
         logoutPropagationContextFunction = Constraint.isNotNull(function, "Function cannot be null");
     }
 
- // Checkstyle: ReturnCount OFF
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         final LogoutPropagationContext logoutPropCtx = logoutPropagationContextFunction.apply(profileRequestContext);
         if (logoutPropCtx == null || logoutPropCtx.getSession() == null) {
             log.error("{} LogoutPropagationContext not found or found with null SPSession", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_POTENTIAL_FLOW);
             return;
         }
 
         final LogoutPropagationFlowDescriptor flow = flowSelectorFunction.apply(logoutPropCtx.getSession());
         if (flow == null) {
-            log.error("{} No potential flows to choose from, logout propagation will fail", getLogPrefix());
+            log.error("{} No potential flows to choose from, no logout propagation possible", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_POTENTIAL_FLOW);
             return;
         }
@@ -101,6 +99,5 @@ public class SelectLogoutPropagationFlow extends AbstractProfileAction {
         log.debug("{} Selecting logout propagation flow {}", getLogPrefix(), flow.getId());
         ActionSupport.buildEvent(profileRequestContext, flow.getId());
     }
- // Checkstyle: ReturnCount ON
     
 }
\ 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