[java-plugin-shibd-saml] branch main updated: WIP on SAML Logout Initiator flow.

Codeberg noreply at shibboleth.net
Tue Apr 7 19:11:50 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-saml.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-saml/commit/567d27aeaf415423b4d9156ef25e7da284e77476

The following commit(s) were added to refs/heads/main by this push:
     new 567d27a  WIP on SAML Logout Initiator flow.
567d27a is described below

commit 567d27aeaf415423b4d9156ef25e7da284e77476
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Tue Apr 7 15:11:39 2026 -0400

    WIP on SAML Logout Initiator flow.
---
 .../sp/saml/saml2/context/SAMLLogoutContext.java   |  78 ++++++
 .../sp/saml/saml2/context/SAMLTokenContext.java    |  24 +-
 .../config/SingleLogoutProfileConfiguration.java   |   3 +
 .../idp/flows/sp/initiator/saml2/saml2-beans.xml   | 122 ---------
 .../sp/logout/initiator/saml2/saml2-beans.xml      |  31 +++
 .../flows/sp/logout/initiator/saml2/saml2-flow.xml |  38 +++
 .../shibboleth/idp/flows/sp/saml2-common-beans.xml | 127 +++++++++-
 .../net/shibboleth/sp/service/agent/postconfig.xml |   2 +
 .../flows/saml2/SAML2TokenConsumerFlowTest.java    |   5 +-
 .../saml/saml2/profile/impl/AddAuthnRequest.java   |   1 -
 .../saml/saml2/profile/impl/AddLogoutRequest.java  | 282 +++++++++++++++++++++
 .../saml2/profile/impl/ExtractSAMLAttributes.java  |   3 +-
 .../saml2/profile/impl/PrepareAgentResponse.java   |  23 +-
 .../impl/ProcessLogoutInitiatorRequest.java        | 193 ++++++++++++++
 .../profile/impl/SAMLTokenContextConsumer.java     |   2 +-
 .../profile/impl/PrepareAgentResponseTest.java     |  15 +-
 .../impl/ProcessLogoutInitiatorRequestTest.java    | 170 +++++++++++++
 17 files changed, 966 insertions(+), 153 deletions(-)

diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLLogoutContext.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLLogoutContext.java
new file mode 100644
index 0000000..c666bb8
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLLogoutContext.java
@@ -0,0 +1,78 @@
+/*
+ * 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.saml.saml2.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.saml.saml2.core.NameID;
+
+/**
+ * Manages state for SAML logout flows.
+ */
+public class SAMLLogoutContext extends BaseContext {
+    
+    /** NameID from original assertion. */
+    @Nullable private NameID nameID;
+    
+    /** Session index. */
+    @Nullable private String sessionIndex;
+        
+    /**
+     * Get the SAML {@link NameID} issued with the session.
+     * 
+     * @return SAML {@link NameID}
+     */
+    @Nullable public NameID getNameID() {
+        return nameID;
+    }
+ 
+    /**
+     * Set the SAML {@link NameID} issued with the session.
+     * 
+     * @param n the SAML {@link NameID}
+     *
+     * @return this context
+     */
+    @Nonnull public SAMLLogoutContext setNameID(@Nullable final NameID n) {
+        nameID = n;
+        
+        return this;
+    }
+    
+    /**
+     * Get the SAML SessionIndex value issued with the session.
+     * 
+     * @return session index
+     */
+    @Nullable public String getSessionIndex() {
+        return sessionIndex;
+    }
+ 
+    /**
+     * Set the SAML SessionIndex value issued with the session.
+     * 
+     * @param index the index
+     * 
+     * @return this context
+     */
+    @Nonnull public SAMLLogoutContext setSessionIndex(@Nullable final String index) {
+        sessionIndex = index;
+        
+        return this;
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
index ffad84d..496f707 100644
--- a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
@@ -18,38 +18,38 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.saml.saml2.core.Assertion;
 import org.opensaml.saml.saml2.core.AuthnStatement;
-import org.opensaml.saml.saml2.core.Subject;
 
 /**
  * Manages state for SAML token consumer flow during final assertion processing.
  */
 public class SAMLTokenContext extends BaseContext {
     
-    /** Subject of assertion used to authenticate. */
-    @Nullable private Subject subject;
+    /** Assertion used to authenticate. */
+    @Nullable private Assertion assertion;
     
     /** Authentication statement. */
     @Nullable private AuthnStatement authnStatement;
         
     /**
-     * Get the SAML {@link Subject} from the authentication.
+     * Get the SAML {@link Assertion} from the authentication.
      * 
-     * @return SAML {@link Subject}
+     * @return SAML {@link Assertion}
      */
-    @Nullable public Subject getSubject() {
-        return subject;
+    @Nullable public Assertion getAssertion() {
+        return assertion;
     }
  
     /**
-     * Set the SAML {@link Subject} from the authentication.
-     * 
-     * @param sub the SAML {@link Subject}
+     * Set the SAML {@link Assertion} from the authentication.
      * 
+     * @param a the SAML {@link Assertion}
+     *
      * @return this context
      */
-    @Nonnull public SAMLTokenContext setSubject(@Nullable final Subject sub) {
-        subject = sub;
+    @Nonnull public SAMLTokenContext setAssertion(@Nullable final Assertion a) {
+        assertion = a;
         
         return this;
     }
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
index 4a1d087..13dc5c2 100644
--- a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
@@ -19,6 +19,8 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.saml.ext.saml2aslo.Asynchronous;
 
+import net.shibboleth.shared.annotation.ConfigurationSetting;
+
 /** Configuration support for SP SAML 2.0 Single Logout. */
 public interface SingleLogoutProfileConfiguration
         extends net.shibboleth.saml.saml2.profile.config.SingleLogoutProfileConfiguration {
@@ -32,6 +34,7 @@ public interface SingleLogoutProfileConfiguration
      * 
      * @return true iff the extension should be included in requests
      */
+    @ConfigurationSetting(name="asynchronous")
     boolean isAsynchronous(@Nullable final ProfileRequestContext profileRequestContext);
     
 }
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
index 3ca6f1e..0a111f9 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
@@ -18,56 +18,9 @@
         p:requireDiscoveryURL="false"
         p:requireRelyingPartyId="true" />
 
-    <bean id="PrepareInboundMessageContext"
-        class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
-        p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
-
-    <bean id="InitializeOutboundMessageContext"
-        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
-        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" />
-
-    <bean id="InitializeMessageChannelSecurityContext" 
-        class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
-        p:confidentialityActive="false"
-        p:integrityActive="false" />
-
     <util:constant id="shibboleth.EndpointType"
         static-field="org.opensaml.saml.saml2.metadata.SingleSignOnService.DEFAULT_ELEMENT_NAME" />
 
-    <util:list id="OutgoingSAML2SPRequestBindings">
-        <ref bean="shibboleth.Binding.SAML2Redirect" />
-        <ref bean="shibboleth.Binding.SAML2POST" />
-        <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
-    </util:list>
-
-    <bean id="OutgoingSAML2SPRequestBindingsStrategy" parent="shibboleth.Functions.Constant"
-        c:_0-ref="OutgoingSAML2SPRequestBindings" />
-
-    <bean id="PopulateBindingAndEndpointContexts"
-        class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
-        p:endpointResolver-ref="shibboleth.EndpointResolver"
-        p:endpointType-ref="shibboleth.EndpointType"
-        p:bindingDescriptorsLookupStrategy-ref="OutgoingSAML2SPRequestBindingsStrategy"
-        p:artifactImpliesSecureChannel="%{sp.artifact.secureChannel:true}" />
-
-    <bean id="PopulateRequestSignatureSigningParameters"
-            class="org.opensaml.saml.common.profile.impl.PopulateSignatureSigningParameters" scope="prototype"
-            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
-            p:signatureSigningParametersResolver-ref="shibboleth.SignatureSigningParametersResolver"
-            p:noResultIsError="false">
-        <property name="activationCondition">
-            <bean class="net.shibboleth.saml.profile.config.logic.SignRequestsPredicate"
-                p:honorMetadata="%{sp.saml.honorWantAuthnRequestsSigned:true}" />
-        </property>
-    </bean>
-
-    <bean id="PopulateEncryptionParameters"
-        class="net.shibboleth.idp.saml.saml2.profile.impl.PopulateEncryptionParameters" scope="prototype"
-        p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
-        p:encryptionParametersResolver-ref="shibboleth.EncryptionParametersResolver"
-        p:protocol-ref="shibboleth.MetadataLookup.Protocol"
-        p:role-ref="shibboleth.MetadataLookup.Role" />
-
     <util:map id="InboundSAML2BindingMap">
         <entry key="POST">
             <util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI" />
@@ -90,55 +43,6 @@
                 p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
         </property>
     </bean>
-    
-    <!-- Default formats not to encrypt. -->
-    <util:set id="DefaultPlaintextNameIDFormats">
-        <util:constant static-field="org.opensaml.saml.saml2.core.NameIDType.ENTITY" />
-    </util:set>
-
-    <bean id="EncryptNameIDs"
-            class="org.opensaml.saml.saml2.profile.impl.EncryptNameIDs" scope="prototype"
-            p:excludedFormats="#{getObject('shibboleth.PlaintextNameIDFormats') ?: getObject('DefaultPlaintextNameIDFormats')}"
-            p:recipientLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
-        <property name="encryptionContextLookupStrategy">
-            <bean parent="shibboleth.Functions.Compose"
-                c:g-ref="shibboleth.ChildLookup.EncryptionParameters"
-                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
-        </property>
-    </bean>
-
-    <bean id="HandleOutboundMessage"
-            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
-            c:messageHandler-ref="PreEncodeMessageHandler"
-            c:executionDirection="OUTBOUND">
-        <property name="errorEvent">
-            <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
-        </property>
-    </bean>
-    
-    <bean id="PreEncodeMessageHandler"
-            class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
-        <property name="handlers">
-            <list>
-                <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
-                    <property name="functionLookupStrategy">
-                        <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
-                    </property>
-                </bean>
-                <bean class="org.opensaml.saml.common.binding.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
-                <bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
-                <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
-                    <property name="activationCondition">
-                        <bean parent="shibboleth.Conditions.NOT">
-                            <constructor-arg>
-                                <bean class="org.opensaml.saml.common.messaging.logic.SignatureCapableBindingPredicate" />
-                            </constructor-arg>
-                        </bean>
-                    </property>
-                </bean>
-            </list>
-        </property>
-    </bean>
 
     <bean id="IssueCorrelationCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
         p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
@@ -158,30 +62,4 @@
         </constructor-arg>
     </bean>
 
-    <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
-
-    <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
-        p:createOutputObjects="true"
-        p:messageEncoderFactory-ref="messageEncoderFactory" />
-
-    <!-- Override IdP's encoders to supply the DDF-backed servlet supplier. -->
-
-    <bean id="shibboleth.Encoders.SAML2RedirectEncoder"
-          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder" scope="prototype" init-method=""
-          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
-
-    <bean id="shibboleth.Encoders.SAML2PostEncoder"
-          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder" scope="prototype" init-method=""
-          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
-          p:velocityEngine-ref="shibboleth.VelocityEngine"
-          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
-          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
-
-    <bean id="shibboleth.Encoders.SAML2PostSimpleSignEncoder"
-          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostSimpleSignEncoder" scope="prototype" init-method=""
-          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
-          p:velocityEngine-ref="shibboleth.VelocityEngine"
-          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
-          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
-
 </beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-beans.xml
new file mode 100644
index 0000000..d3caea4
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-beans.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <util:constant id="shiibboleth.sp.ProfileId"
+        static-field="net.shibboleth.saml.saml2.profile.config.SingleLogoutProfileConfiguration.PROFILE_ID" />
+
+    <import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml" />
+
+    <bean id="ProcessLogoutInitiatorRequest"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.ProcessLogoutInitiatorRequest" scope="prototype"
+        p:parserPool-ref="shibboleth.ParserPool" />
+
+    <util:constant id="shibboleth.EndpointType"
+        static-field="org.opensaml.saml.saml2.metadata.SingleLogoutService.DEFAULT_ELEMENT_NAME" />
+
+    <bean id="AddLogoutRequest"
+            class="net.shibboleth.sp.saml.saml2.profile.impl.AddLogoutRequest" scope="prototype"
+            p:overwriteExisting="true">
+        <property name="identifierGeneratorLookupStrategy">
+            <bean class="net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction"
+                p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
+        </property>
+    </bean>
+    
+</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml
new file mode 100644
index 0000000..18f438f
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml
@@ -0,0 +1,38 @@
+<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/logout/initiator">
+
+    <action-state id="SAML2LogoutInitiator">
+        <evaluate expression="ProcessLogoutInitiatorRequest" />
+        <evaluate expression="PrepareInboundMessageContext" />
+        <evaluate expression="SAMLProtocolAndRole" />
+        <evaluate expression="SAMLMetadataLookup" />
+        
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="InitializeMessageChannelSecurityContext" />
+        <evaluate expression="PopulateBindingAndEndpointContexts" />
+
+        <evaluate expression="PopulateRequestSignatureSigningParameters" />
+        <evaluate expression="PopulateEncryptionParameters" />
+        
+        <evaluate expression="AddLogoutRequest" />
+        <evaluate expression="EncryptNameIDs" />
+
+        <evaluate expression="HandleOutboundMessage" />
+        <evaluate expression="EncodeMessage" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="proceed" />
+        <!-- Remap any other events into a fall-through to the next flow. -->
+        <transition to="ReselectFlow" />
+    </action-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/initiator/saml2/saml2-beans.xml" />
+
+</flow>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
index 75423c6..a56652c 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
@@ -21,6 +21,10 @@
         </property>
     </bean>
 
+    <bean id="PrepareInboundMessageContext"
+        class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
+        p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+
     <util:constant id="shibboleth.MetadataLookup.Protocol"
         static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML20P_NS" />
 
@@ -45,6 +49,32 @@
         </constructor-arg>
     </bean>
 
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
+        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" />
+
+    <bean id="InitializeMessageChannelSecurityContext" 
+        class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
+        p:confidentialityActive="false"
+        p:integrityActive="false" />
+
+    <util:list id="OutgoingSAML2SPRequestBindings">
+        <ref bean="shibboleth.Binding.SAML2Redirect" />
+        <ref bean="shibboleth.Binding.SAML2POST" />
+        <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
+    </util:list>
+
+    <bean id="OutgoingSAML2SPRequestBindingsStrategy" parent="shibboleth.Functions.Constant"
+        c:_0-ref="OutgoingSAML2SPRequestBindings" />
+
+    <bean id="PopulateBindingAndEndpointContexts"
+        class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
+        p:endpointResolver-ref="shibboleth.EndpointResolver"
+        p:endpointType-ref="shibboleth.EndpointType"
+        p:bindingDescriptorsLookupStrategy-ref="OutgoingSAML2SPRequestBindingsStrategy"
+        p:artifactImpliesSecureChannel="%{sp.artifact.secureChannel:true}" />
+
+
     <bean id="InitializeRelyingPartyContextFromSAMLPeer"
         class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype" />
 
@@ -55,6 +85,27 @@
         class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
         p:profileId-ref="shiibboleth.sp.ProfileId" />
 
+    <bean id="PopulateInboundMessageContextWithSAMLSelf"
+        class="net.shibboleth.idp.saml.profile.impl.PopulateInboundMessageContextWithSAMLSelf" scope="prototype" />
+    
+    <bean id="PopulateRequestSignatureSigningParameters"
+            class="org.opensaml.saml.common.profile.impl.PopulateSignatureSigningParameters" scope="prototype"
+            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+            p:signatureSigningParametersResolver-ref="shibboleth.SignatureSigningParametersResolver"
+            p:noResultIsError="false">
+        <property name="activationCondition">
+            <bean class="net.shibboleth.saml.profile.config.logic.SignRequestsPredicate"
+                p:honorMetadata="%{sp.saml.honorWantAuthnRequestsSigned:true}" />
+        </property>
+    </bean>
+
+    <bean id="PopulateEncryptionParameters"
+        class="net.shibboleth.idp.saml.saml2.profile.impl.PopulateEncryptionParameters" scope="prototype"
+        p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
+        p:encryptionParametersResolver-ref="shibboleth.EncryptionParametersResolver"
+        p:protocol-ref="shibboleth.MetadataLookup.Protocol"
+        p:role-ref="shibboleth.MetadataLookup.Role" />
+
     <bean id="PopulateSignatureValidationParameters"
         class="org.opensaml.profile.action.impl.PopulateSignatureValidationParameters" scope="prototype"
         p:configurationLookupStrategy-ref="shibboleth.SignatureValidationConfigurationLookup"
@@ -65,7 +116,79 @@
         p:configurationLookupStrategy-ref="shibboleth.ClientTLSValidationConfigurationLookup"
         p:clientTLSValidationParametersResolver-ref="shibboleth.ClientTLSValidationParametersResolver" />
 
-    <bean id="PopulateInboundMessageContextWithSAMLSelf"
-        class="net.shibboleth.idp.saml.profile.impl.PopulateInboundMessageContextWithSAMLSelf" scope="prototype" />
+    <!-- Default formats not to encrypt. -->
+    <util:set id="DefaultPlaintextNameIDFormats">
+        <util:constant static-field="org.opensaml.saml.saml2.core.NameIDType.ENTITY" />
+    </util:set>
 
+    <bean id="EncryptNameIDs"
+            class="org.opensaml.saml.saml2.profile.impl.EncryptNameIDs" scope="prototype"
+            p:excludedFormats="#{getObject('shibboleth.PlaintextNameIDFormats') ?: getObject('DefaultPlaintextNameIDFormats')}"
+            p:recipientLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
+        <property name="encryptionContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.ChildLookup.EncryptionParameters"
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
+    </bean>
+
+    <bean id="HandleOutboundMessage"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:messageHandler-ref="PreEncodeMessageHandler"
+            c:executionDirection="OUTBOUND">
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+        </property>
+    </bean>
+    
+    <bean id="PreEncodeMessageHandler"
+            class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
+        <property name="handlers">
+            <list>
+                <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+                    <property name="functionLookupStrategy">
+                        <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
+                    </property>
+                </bean>
+                <bean class="org.opensaml.saml.common.binding.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
+                <bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
+                <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
+                    <property name="activationCondition">
+                        <bean parent="shibboleth.Conditions.NOT">
+                            <constructor-arg>
+                                <bean class="org.opensaml.saml.common.messaging.logic.SignatureCapableBindingPredicate" />
+                            </constructor-arg>
+                        </bean>
+                    </property>
+                </bean>
+            </list>
+        </property>
+    </bean>
+
+    <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
+
+    <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
+        p:createOutputObjects="true"
+        p:messageEncoderFactory-ref="messageEncoderFactory" />
+
+    <!-- Override IdP's encoders to supply the DDF-backed servlet supplier. -->
+
+    <bean id="shibboleth.Encoders.SAML2RedirectEncoder"
+          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
+
+    <bean id="shibboleth.Encoders.SAML2PostEncoder"
+          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+          p:velocityEngine-ref="shibboleth.VelocityEngine"
+          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
+          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
+    <bean id="shibboleth.Encoders.SAML2PostSimpleSignEncoder"
+          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostSimpleSignEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+          p:velocityEngine-ref="shibboleth.VelocityEngine"
+          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
+          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+    
 </beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index 0e2882b..a7fbdcd 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -22,6 +22,7 @@
             p:order="%{sp.saml.relativeOrder:1}"
             p:metadataResolver-ref="shibboleth.MetadataResolverService"
             p:sessionInitiators="saml2"
+            p:logoutInitiators="saml2"
             p:tokenConsumers="#{{ 'saml2/post', 'saml2/post-simplesign', 'saml2/artifact' }}">
         <property name="id">
             <util:constant static-field="net.shibboleth.sp.saml.saml2.SAML2ProtocolSupportService.PROTOCOL_ID" />
@@ -29,6 +30,7 @@
         <property name="defaultProfileConfigurations">
             <list>
                 <ref bean="SAML2.SSO" />
+                <ref bean="SAML2.Logout" />
             </list>
         </property>
         <property name="metadataDrivenDefaultProfileConfigurations">
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
index b23b78b..a589534 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
@@ -82,6 +82,7 @@ import net.shibboleth.sp.flows.AbstractSPFlowTest;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.profile.impl.PrepareAgentResponse;
 
 /**
  * Unit test for the SP session-initiator flow.
@@ -616,7 +617,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
             final DDF s = output.getmember(ConsumerConstants.SESSION_OPAQUE);
             assert s != null;
             Assert.assertTrue(s.isstruct());
-            final DDF nameIdDdf = s.getmember("nameID");
+            final DDF nameIdDdf = s.getmember(PrepareAgentResponse.NAMEID_PARAM);
             Assert.assertTrue(nameIdDdf.isstring());
             final String nameIdString = nameIdDdf.string();
             assert nameIdString != null;
@@ -626,7 +627,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
                 if (obj instanceof final NameID nameID) {
                     Assert.assertEquals(nameID.getValue(), "jdoe at example.org");
                     Assert.assertEquals(nameID.getFormat(), NameIDType.EMAIL);
-                    Assert.assertEquals(nameID.getSPProvidedID(), sessionIndex);
+                    Assert.assertEquals(nameID.getSPProvidedID(), ISSUER + "!!" + sessionIndex);
                 } else {
                     Assert.fail("Session data was not a NameID");
                 }
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
index 66d030a..055c45f 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -586,7 +586,6 @@ public class AddAuthnRequest extends AbstractApplicationAction {
      */
     @Nullable private Extensions buildExtensions(@Nonnull final ProfileRequestContext profileRequestContext) {
                 
-        assert profileConfiguration!=null;
         final Collection<RequestedAttribute> attrs = profileConfiguration.getRequestedAttributes(profileRequestContext);
         if (!attrs.isEmpty()) {
             final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java
new file mode 100644
index 0000000..011a05b
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.saml.saml2.profile.impl;
+
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.saml.saml2.context.SAMLLogoutContext;
+import net.shibboleth.sp.saml.saml2.profile.config.SingleLogoutProfileConfiguration;
+
+import org.opensaml.core.xml.XMLObjectBuilderFactory;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.common.SAMLVersion;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+import org.opensaml.saml.ext.saml2aslo.Asynchronous;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.Extensions;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.SessionIndex;
+import org.slf4j.Logger;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that creates an {@link LogoutRequest} and sets it as the message returned by
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ * 
+ * <p>If an issuer value is returned via a lookup strategy, then it's set as the Issuer of the message.</p>
+ * 
+ * <p>A {@link SAMLLogoutContext} must be present to provide the necessary information to populate into
+ * the request.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#IO_ERROR}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * 
+ * @pre ProfileRequestContext.getSubcontext(SAMLLogoutContext.class) != null
+ * @post ProfileRequestContext.getOutboundMessageContext().getMessage() != null
+ */
+public class AddLogoutRequest extends AbstractApplicationAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AddLogoutRequest.class);
+    
+    /** Overwrite an existing message? */
+    private boolean overwriteExisting;
+
+    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+    @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+    
+    /** Strategy used to obtain the request issuer value. */
+    @Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
+    
+    /** The generator to use. */
+    @NonnullBeforeExec private IdentifierGenerationStrategy idGenerator;
+    
+    /** Applicable profile configuration. */
+    @NonnullBeforeExec private SingleLogoutProfileConfiguration profileConfiguration;
+
+    /** Cached logout context. */
+    @NonnullBeforeExec private SAMLLogoutContext logoutContext;
+    
+    /** EntityID to populate into Issuer element. */
+    @Nullable private String issuerId;
+    
+    /** Constructor. */
+    public AddLogoutRequest() {
+        // Default strategy is a 16-byte secure random source.
+        idGeneratorLookupStrategy = new IdentifierGenerationStrategyLookupFunction();
+
+        issuerLookupStrategy = new IssuerLookupFunction();
+    }
+        
+    /**
+     * Set whether to overwrite an existing message.
+     * 
+     * @param flag flag to set
+     */
+    public void setOverwriteExisting(final boolean flag) {
+        checkSetterPreconditions();
+        overwriteExisting = flag;
+    }
+
+    /**
+     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+        checkSetterPreconditions();
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the issuer value to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nullable final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        issuerLookupStrategy = strategy;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        logoutContext = profileRequestContext.getSubcontext(SAMLLogoutContext.class);
+        if (logoutContext == null || logoutContext.getNameID() == null) {
+            log.error("{} No populated SAMLLogoutContext available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+                
+        final RelyingPartyContext rpCtx = profileRequestContext.getSubcontext(RelyingPartyContext.class);
+        if (rpCtx != null && rpCtx.getProfileConfig() instanceof SingleLogoutProfileConfiguration slo) {
+            profileConfiguration = slo;
+        }
+        if (profileConfiguration == null) {
+            log.error("{} SingleLogoutProfileConfiguration not found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        final MessageContext outboundMessageCtx = profileRequestContext.getOutboundMessageContext();
+        if (outboundMessageCtx == null) {
+            log.debug("{} No outbound message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        } else if (!overwriteExisting && outboundMessageCtx.getMessage() != null) {
+            log.debug("{} Outbound message context already contains a message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+        if (idGenerator == null) {
+            log.debug("{} No identifier generation strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        if (issuerLookupStrategy != null) {
+            issuerId = issuerLookupStrategy.apply(profileRequestContext);
+        }
+
+        outboundMessageCtx.setMessage(null);
+        
+        return true;
+    }
+
+// Checkstyle: MethodLength OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+        final SAMLObjectBuilder<LogoutRequest> requestBuilder =
+                (SAMLObjectBuilder<LogoutRequest>) bf.<LogoutRequest>ensureBuilder(
+                        LogoutRequest.DEFAULT_ELEMENT_NAME);
+
+        final LogoutRequest object = requestBuilder.buildObject();
+        object.setID(idGenerator.generateIdentifier());
+        object.setIssueInstant(Instant.now());
+        object.setVersion(SAMLVersion.VERSION_20);
+
+        if (issuerId != null) {
+            log.debug("{} Setting Issuer to {}", getLogPrefix(), issuerId);
+            final SAMLObjectBuilder<Issuer> issuerBuilder =
+                    (SAMLObjectBuilder<Issuer>) bf.<Issuer>ensureBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+            final Issuer issuer = issuerBuilder.buildObject();
+            issuer.setValue(issuerId);
+            object.setIssuer(issuer);
+        } else {
+            log.debug("{} No issuer value available, leaving Issuer unset", getLogPrefix());
+        }
+        
+        final NameID original = logoutContext.getNameID();
+        assert original != null;
+        try {
+            final NameID cloned = XMLObjectSupport.cloneXMLObject(original);
+            object.setNameID(cloned);
+            log.debug("{} Populating LogoutRequest with NameID '{}', Format '{}', SessionIndex '{}'", getLogPrefix(),
+                    cloned.getValue(), cloned.getFormat(), logoutContext.getSessionIndex());
+            
+        } catch (final MarshallingException | UnmarshallingException e) {
+            log.error("{} Error cloning NameID for insertion", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return;
+        }
+
+        if (logoutContext.getSessionIndex() != null) {
+            final SAMLObjectBuilder<SessionIndex> indexBuilder =
+                    (SAMLObjectBuilder<SessionIndex>) bf.<SessionIndex>ensureBuilder(
+                            SessionIndex.DEFAULT_ELEMENT_NAME);
+            final SessionIndex index = indexBuilder.buildObject();
+            index.setValue(logoutContext.getSessionIndex());
+            object.getSessionIndexes().add(index);
+        }
+        
+        object.setExtensions(buildExtensions(profileRequestContext));
+        
+        profileRequestContext.ensureOutboundMessageContext().setMessage(object);
+        
+        // Check for RelayState.
+        final DDF input = ensureAgentRequestContext().getInput();
+        final String relayState = input != null ? input.getmember(SPConstants.STATE).string() : null;
+        if (relayState != null) {
+            SAMLBindingSupport.setRelayState(profileRequestContext.ensureOutboundMessageContext(), relayState);
+        }
+        
+        log.info("{} Generated LogoutRequest with ID {} from {}", getLogPrefix(), object.getID(), issuerId);
+    }
+     
+    /**
+     * Build {@link Asynchronous} extension if required.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return extension or null
+     */
+    @Nullable private Extensions buildExtensions(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (profileConfiguration.isAsynchronous(profileRequestContext)) {
+            final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+            final SAMLObjectBuilder<Extensions> extBuilder =
+                    (SAMLObjectBuilder<Extensions>) bf.<Extensions>ensureBuilder(
+                            Extensions.DEFAULT_ELEMENT_NAME);
+            final SAMLObjectBuilder<Asynchronous> asyncBuilder =
+                    (SAMLObjectBuilder<Asynchronous>) bf.<Asynchronous>ensureBuilder(
+                            Asynchronous.DEFAULT_ELEMENT_NAME);
+
+            final Extensions ext = extBuilder.buildObject();
+            ext.getUnknownXMLObjects().add(asyncBuilder.buildObject());
+            return ext;
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
index 7a327df..f5d8676 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
@@ -362,7 +362,8 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
         final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
         assert mapped != null;
 
-        final Subject subject = samlTokenContext.getSubject();
+        final Assertion primary = samlTokenContext.getAssertion();
+        final Subject subject = primary != null ? primary.getSubject() : null;
         final NameID nameID = subject != null ? subject.getNameID() : null;
         
         final AuthnStatement theStatement = samlTokenContext.getAuthnStatement();
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
index 68e3a93..be30ad1 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
@@ -27,13 +27,16 @@ 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.opensaml.saml.saml2.core.Assertion;
 import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Issuer;
 import org.opensaml.saml.saml2.core.NameID;
 import org.opensaml.saml.saml2.core.Subject;
 import org.slf4j.Logger;
 
 import net.shibboleth.idp.attribute.context.AttributeContext;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -60,6 +63,9 @@ import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
  */
 public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
 
+    /** Parameter for accessing NameID from opaque data. */
+    @Nonnull @NotEmpty public static final String NAMEID_PARAM = "NameID";
+    
     /** DOM configuration parameters used by LSSerializer to exclude XML declaration. */
     @Nonnull private static final Map<String, Object> NO_XML_DECL_PARAMS;
     
@@ -115,23 +121,28 @@ public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
     @Override
     @Nullable protected DDF getSessionData(@Nonnull final ProfileRequestContext profileRequestContext) {
         
-        final Subject subject = samlTokenContext.getSubject();
+        final Assertion assertion = samlTokenContext.getAssertion();
+        final Issuer issuer = assertion != null ? assertion.getIssuer() : null;
+        final Subject subject = assertion != null ? assertion.getSubject() : null;
         final NameID nameID = subject != null ? subject.getNameID() : null;
-        if (nameID == null) {
-            log.debug("{} No NameID found in assertion, no session data to attach", getLogPrefix());
+        
+        if (issuer == null || issuer.getValue() == null || nameID == null || nameID.getValue() == null) {
+            log.debug("{} No Issuer or NameID found in Assertion, no session data to attach", getLogPrefix());
             return null;
         }
             
         final AuthnStatement statement = samlTokenContext.getAuthnStatement();
         final String sessionIndex = statement != null ? statement.getSessionIndex() : null;
+        // If you tell anyone I did this, I will be sad.
         if (sessionIndex != null) {
-            // If you tell anyone I did this, I will be sad.
-            nameID.setSPProvidedID(sessionIndex);
+            nameID.setSPProvidedID(issuer.getValue() + "!!" + sessionIndex);
+        } else {
+            nameID.setSPProvidedID(issuer.getValue());
         }
         
         try {
             final String xml = SerializeSupport.nodeToString(XMLObjectSupport.marshall(nameID), NO_XML_DECL_PARAMS);
-            return new DDF("nameID").string(Base64Support.encodeURLSafe(xml.getBytes(StandardCharsets.UTF_8)));
+            return new DDF(NAMEID_PARAM).string(Base64Support.encodeURLSafe(xml.getBytes(StandardCharsets.UTF_8)));
         } catch (final MarshallingException | EncodingException e) {
             log.error("{} Error marshalling and encoding NameID", getLogPrefix(), e);
         }
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequest.java
new file mode 100644
index 0000000..a707ff9
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.saml.saml2.profile.impl;
+
+import java.io.StringReader;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+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.opensaml.saml.saml2.core.NameID;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.xml.ParserPool;
+import net.shibboleth.shared.xml.XMLParserException;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.context.SAMLLogoutContext;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Processes a request to "possibly" initiate a SAML 2.0 logout by examining the input to recover
+ * the required information from the opaque portion of the session created by the token consumer
+ * flow.
+ * 
+ * <p>Assuming the opaque data is present and sufficient, it creates a {@link RelyingPartyContext}
+ * based on the identity of the original IdP that issued the token that led to the session.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#UNABLE_TO_DECODE}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ */
+public class ProcessLogoutInitiatorRequest extends AbstractApplicationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessLogoutInitiatorRequest.class);
+    
+    /** Parser machinery. */
+    @NonnullAfterInit private ParserPool parserPool;
+    
+    /** Creation strategy for {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextCreationStrategy;
+    
+    /** The relying party name to base the inbound context on. */
+    @NonnullBeforeExec private String relyingPartyId;
+    
+    /** NameID from session. */
+    @NonnullBeforeExec private NameID nameID;
+    
+    /** Session index from session. */
+    @Nullable private String sessionIndex;
+
+    /** Constructor. */
+    public ProcessLogoutInitiatorRequest() {
+        relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
+    }
+    
+    /**
+     * Sets the {@link ParserPool} to parse session data with.
+     * 
+     * @param pool parser pool
+     */
+    public void setParserPool(@Nonnull final ParserPool pool) {
+        checkSetterPreconditions();
+        
+        parserPool = pool;
+    }
+    
+    /**
+     * Set an optional lookup strategy to identify the relying party name, as a substitute for the session/logout
+     * assumptions made by the action otherwise.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+        
+        relyingPartyContextCreationStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (parserPool == null) {
+            throw new ComponentInitializationException("ParserPool cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        log.debug("{} Evaluating applicability of request to saml2 logout initiator flow", getLogPrefix());
+        
+        final DDF input = ensureAgentRequestContext().getInput();
+        if (input == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            log.error("{} No input message from agent", getLogPrefix());
+            return false;
+        }
+        
+        final String pickled = input.getmember(ConsumerConstants.SESSION_OPAQUE).getmember(PrepareAgentResponse.NAMEID_PARAM).string();
+        if (pickled == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+            log.debug("{} No encoded NameID found in input message", getLogPrefix());
+            return false;
+        }
+        
+        // Decode NameID from session data from Agent and unpack the buried information.
+        try {
+            final XMLObject xmlObject = XMLObjectSupport.unmarshallFromReader(parserPool, new StringReader(pickled));
+            if (xmlObject instanceof NameID n) {
+                final String buriedData = n.getSPProvidedID();
+                if (buriedData != null) {
+                    final int index = buriedData.indexOf("!!");
+                    if (index > 0) {
+                        relyingPartyId = buriedData.substring(0, index);
+                        sessionIndex = buriedData.substring(index + 2);
+                    } else {
+                        relyingPartyId = buriedData;
+                    }
+                    nameID = n;
+                } else {
+                    throw new XMLParserException("Decoded NameID did not contain required data for logout.");
+                }
+            } else {
+                throw new XMLParserException("Decoded object was of unexpected type.");
+            }
+        } catch (final XMLParserException | UnmarshallingException e) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+            log.error("{} Failed to decode session information", getLogPrefix(), e);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final SAMLLogoutContext logoutContext = profileRequestContext.ensureSubcontext(SAMLLogoutContext.class);
+        nameID.setSPProvidedID(null);
+        logoutContext.setNameID(nameID);
+        logoutContext.setSessionIndex(sessionIndex);
+        
+        final RelyingPartyContext rpContext = relyingPartyContextCreationStrategy.apply(profileRequestContext);
+        if (rpContext == null) {
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            log.error("{} Unable to create RelyingPartyContext", getLogPrefix());
+            return;
+        }
+
+        rpContext.setRelyingPartyId(relyingPartyId);
+        log.debug("{} Initialized RelyingPartyContext for {}", getLogPrefix(), relyingPartyId);
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
index 6a2ab23..9f35ffe 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
@@ -48,7 +48,7 @@ public class SAMLTokenContextConsumer implements BiConsumer<ProfileRequestContex
         tokenContext.setAuthnStatement(statement);
 
         if (statement.getParent() instanceof Assertion assertion) {
-            tokenContext.setSubject(assertion.getSubject());
+            tokenContext.setAssertion(assertion);
         }
     }
 
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
index 9e26644..d4a3064 100644
--- a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
@@ -145,7 +145,7 @@ public class PrepareAgentResponseTest extends BaseAgplicationActionTest {
         
         final DDF state = out.getmember(ConsumerConstants.SESSION_OPAQUE);        
         Assert.assertTrue(state.isstruct());
-        final DDF nameId = state.getmember("nameID");
+        final DDF nameId = state.getmember(PrepareAgentResponse.NAMEID_PARAM);
         Assert.assertTrue(nameId.isstring());
         
         final String encoded = nameId.string();
@@ -160,7 +160,7 @@ public class PrepareAgentResponseTest extends BaseAgplicationActionTest {
             parserPool.destroy();
             if (xmlobj instanceof final NameID nameID) {
                 Assert.assertEquals(nameID.getValue(), "jdoe");
-                Assert.assertEquals(nameID.getSPProvidedID(), "foo");
+                Assert.assertEquals(nameID.getSPProvidedID(), "foo!!foo");
             } else {
                 Assert.fail("XMLObject stored in NameID field was not a NameID");
             }
@@ -279,17 +279,20 @@ public class PrepareAgentResponseTest extends BaseAgplicationActionTest {
      * Adds mock assertion content to the SAMLTokenContext.
      */
     private void buildAssertion() {
-        final Subject subject = SAML2ActionTestingSupport.buildSubject("jdoe");
-        samlTokenContext.setSubject(subject);
-        
         final AuthnStatement statement = SAML2ActionTestingSupport.buildAuthnStatement();
         statement.setSessionIndex("foo");
         
         samlTokenContext.setAuthnStatement(statement);
         
         final Assertion assertion = SAML2ActionTestingSupport.buildAssertion();
-        assertion.setSubject(subject);
         assertion.getAuthnStatements().add(statement);
+        
+        assertion.setIssuer(SAML2ActionTestingSupport.buildIssuer("foo"));
+        
+        final Subject subject = SAML2ActionTestingSupport.buildSubject("jdoe");
+        assertion.setSubject(subject);
+
+        samlTokenContext.setAssertion(assertion);
     }
     
 }
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequestTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequestTest.java
new file mode 100644
index 0000000..a9d4f25
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequestTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.saml.saml2.profile.impl;
+
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.saml.saml2.core.NameID;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.xml.impl.BasicParserPool;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.saml.saml2.context.SAMLLogoutContext;
+
+/**
+ * Unit test for {@link ProcessLogoutInitiatorRequest} action.
+ */
+ at SuppressWarnings("javadoc")
+public class ProcessLogoutInitiatorRequestTest extends BaseAgplicationActionTest {
+
+    private ProcessLogoutInitiatorRequest action;
+        
+    /**
+     * Set up test.
+     * 
+     * @throws ComponentInitializationException
+     */
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.beforeMethod();
+        prc.removeSubcontext(RelyingPartyContext.class);
+        
+        final BasicParserPool pool = new BasicParserPool();
+        pool.initialize();
+        
+        action = new ProcessLogoutInitiatorRequest();
+        action.setParserPool(pool);
+        action.initialize();
+    }
+    
+    /**
+     * Tear down test.
+     */
+    @AfterMethod
+    public void tearDown() {
+        action.destroy();
+    }
+    
+    
+    @Test(expectedExceptions=ComponentInitializationException.class)
+    public void testNoParserPool() throws ComponentInitializationException {
+        new ProcessLogoutInitiatorRequest().initialize();
+    }
+
+    @Test
+    public void testNoInput() throws ComponentInitializationException {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+        Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+    }
+
+    @Test
+    public void testNoSessionData() throws ComponentInitializationException {
+        final DDF input = new DDF(null).structure();
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+        Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+    }
+
+    @Test
+    public void testMisnamedSessionData() throws ComponentInitializationException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember("foo").string("bar");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+        Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+    }
+    
+    @Test
+    public void testInvalidSessionData() throws ComponentInitializationException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string("bar");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+        Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+    }
+
+    @Test
+    public void testIncompleteSessionData() throws ComponentInitializationException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+                "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='bar'>foo</NameID>");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+        Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+    }
+
+    @Test
+    public void testNoIndex() throws ComponentInitializationException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+                "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='bar' SPProvidedID='https://idp.example.org/idp'>foo</NameID>");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertEquals(prc.ensureSubcontext(RelyingPartyContext.class).getRelyingPartyId(), "https://idp.example.org/idp");
+        
+        final SAMLLogoutContext logoutContext = prc.getSubcontext(SAMLLogoutContext.class);
+        assert logoutContext != null;
+        Assert.assertNull(logoutContext.getSessionIndex());
+        
+        final NameID nameID = logoutContext.getNameID();
+        assert nameID != null;
+        Assert.assertEquals(nameID.getValue(), "foo");
+        Assert.assertEquals(nameID.getFormat(), "bar");
+        Assert.assertNull(nameID.getSPProvidedID());
+    }
+
+    @Test
+    public void testIndex() throws ComponentInitializationException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+                "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='bar' SPProvidedID='https://idp.example.org/idp!!12345'>foo</NameID>");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertEquals(prc.ensureSubcontext(RelyingPartyContext.class).getRelyingPartyId(), "https://idp.example.org/idp");
+        
+        final SAMLLogoutContext logoutContext = prc.getSubcontext(SAMLLogoutContext.class);
+        assert logoutContext != null;
+        Assert.assertEquals(logoutContext.getSessionIndex(), "12345");
+        
+        final NameID nameID = logoutContext.getNameID();
+        assert nameID != null;
+        Assert.assertEquals(nameID.getValue(), "foo");
+        Assert.assertEquals(nameID.getFormat(), "bar");
+        Assert.assertNull(nameID.getSPProvidedID());
+    }
+
+}
\ 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