[java-identity-provider] branch master updated: IDP-929 - Extend logout behavioral options

Scott Cantor cantor.2 at osu.edu
Wed Dec 19 16:20:30 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=1a45cb8fadf40a1da958e35ad8d4c1abc188fb4a

The following commit(s) were added to refs/heads/master by this push:
       new  1a45cb8   IDP-929 - Extend logout behavioral options
1a45cb8 is described below

commit 1a45cb8fadf40a1da958e35ad8d4c1abc188fb4a
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Dec 19 16:20:27 2018 -0500

    IDP-929 - Extend logout behavioral options
    
    https://issues.shibboleth.net/jira/browse/IDP-929
    
    Adds support for user prompt to cancel logout.
---
 idp-conf/src/main/resources/conf/idp.properties    |   3 +
 .../main/resources/system/conf/audit-system.xml    |  13 ++
 .../resources/system/flows/logout/logout-beans.xml |   8 +-
 .../resources/system/flows/logout/logout-flow.xml  |  38 ++++-
 .../system/flows/saml/saml2/slo-back-beans.xml     |   5 +-
 .../system/flows/saml/saml2/slo-back-flow.xml      |   1 +
 .../flows/saml/saml2/slo-front-abstract-beans.xml  |  40 ++---
 .../flows/saml/saml2/slo-front-abstract-flow.xml   |  36 ++++-
 .../resources/system/messages/messages.properties  |  18 ++-
 .../src/main/resources/views/logout-complete.vm    |   8 +
 idp-conf/src/main/resources/views/logout.vm        |  76 ++++++---
 .../saml2/profile/impl/ProcessLogoutRequest.java   |  48 ++----
 .../profile/impl/ProcessLogoutRequestTest.java     |  50 ++++--
 .../idp/session/context/LogoutContext.java         |  19 ++-
 .../idp/session/impl/DestroySessions.java          | 178 +++++++++++++++++++++
 .../session/impl/LogoutStatusStrategyFunction.java |  78 +++++++++
 .../shibboleth/idp/session/impl/ProcessLogout.java |  28 +---
 ...essLogoutTest.java => DestroySessionsTest.java} | 149 +++++++++++------
 .../idp/session/impl/ProcessLogoutTest.java        |  29 +++-
 19 files changed, 625 insertions(+), 200 deletions(-)

diff --git a/idp-conf/src/main/resources/conf/idp.properties b/idp-conf/src/main/resources/conf/idp.properties
index 0d1610d..e633f07 100644
--- a/idp-conf/src/main/resources/conf/idp.properties
+++ b/idp-conf/src/main/resources/conf/idp.properties
@@ -174,6 +174,9 @@ idp.authn.flows = Password
 # Whether to require logout requests/responses be signed/authenticated.
 #idp.logout.authenticated = true
 
+# Bean to determine whether user should be allowed to cancel logout
+#idp.logout.promptUser=shibboleth.Conditions.FALSE
+
 # Message freshness and replay cache tuning
 #idp.policy.messageLifetime = PT3M
 #idp.policy.clockSkew = PT3M
diff --git a/idp-conf/src/main/resources/system/conf/audit-system.xml b/idp-conf/src/main/resources/system/conf/audit-system.xml
index 7eb009a..acb49dd 100644
--- a/idp-conf/src/main/resources/system/conf/audit-system.xml
+++ b/idp-conf/src/main/resources/system/conf/audit-system.xml
@@ -537,6 +537,19 @@
                         </constructor-arg>
                     </bean>
                 </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.profile.IdPAuditFields.SESSION_ID"/>
+                    </key>
+                    <bean class="com.google.common.base.Functions" factory-method="compose">
+                        <constructor-arg name="g">
+                            <bean class="net.shibboleth.idp.session.context.navigate.SessionContextIDLookupFunction" />
+                        </constructor-arg>
+                        <constructor-arg name="f">
+                            <ref bean="shibboleth.ChildLookup.SessionContext" />
+                        </constructor-arg>
+                    </bean>
+                </entry>
             </map>
         </property>
     </bean>
diff --git a/idp-conf/src/main/resources/system/flows/logout/logout-beans.xml b/idp-conf/src/main/resources/system/flows/logout/logout-beans.xml
index 6417a19..0ff764b 100644
--- a/idp-conf/src/main/resources/system/flows/logout/logout-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/logout-beans.xml
@@ -21,6 +21,8 @@
     <!-- Declares audit field extractor functions injected into beans that populate the audit context. -->
     <import resource="../../conf/audit-system.xml" />
 
+    <alias alias="UserPromptCondition" name="%{idp.logout.promptUser:shibboleth.Conditions.FALSE}" />
+
     <bean id="InitializeProfileRequestContext"
         class="net.shibboleth.idp.profile.impl.InitializeProfileRequestContext" scope="prototype"
         p:profileId="http://shibboleth.net/ns/profiles/logout"
@@ -42,11 +44,13 @@
     <bean id="ProcessLogout"
         class="net.shibboleth.idp.session.impl.ProcessLogout" scope="prototype"
         p:httpServletRequest-ref="shibboleth.HttpServletRequest"
-        p:httpServletResponse-ref="shibboleth.HttpServletResponse"
-        p:sessionManager-ref="shibboleth.SessionManager"
         p:sessionResolver-ref="shibboleth.SessionManager"
         p:checkAddressCondition="#{getObject('%{idp.session.consistentAddressCondition:null}'.trim()) ?: %{idp.session.consistentAddress:true}}" />
     
+    <bean id="DestroySessions"
+        class="net.shibboleth.idp.session.impl.DestroySessions" scope="prototype"
+        p:sessionManager-ref="shibboleth.SessionManager" />
+    
     <bean id="PopulateClientStorageSaveContext"
         class="org.opensaml.storage.impl.client.PopulateClientStorageSaveContext" scope="prototype"
         p:storageServices="#{ getObject('shibboleth.ClientStorageServices') ?: getObject('shibboleth.DefaultClientStorageServices') }" />
diff --git a/idp-conf/src/main/resources/system/flows/logout/logout-flow.xml b/idp-conf/src/main/resources/system/flows/logout/logout-flow.xml
index ec124c5..a4ec03a 100644
--- a/idp-conf/src/main/resources/system/flows/logout/logout-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/logout-flow.xml
@@ -28,6 +28,22 @@
 
     <action-state id="DoLogout">
         <evaluate expression="ProcessLogout" />
+        <evaluate expression="LogoutPopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckPromptCondition1">
+            <set name="flowScope.transitionAfterDestroy" value="'ContinueLogout'" />
+            <set name="flowScope.promptForIdP" value="UserPromptCondition.apply(opensamlProfileRequestContext)" />
+        </transition>
+    </action-state>
+
+    <decision-state id="CheckPromptCondition1">
+        <if test="promptForIdP"
+            then="ContinueLogout" else="DestroySessions" />
+    </decision-state>
+
+    <action-state id="DestroySessions">
+        <evaluate expression="DestroySessions" />
         <evaluate expression="'proceed'" />
         
         <transition on="proceed" to="PopulateClientStorageSaveContext" />
@@ -40,18 +56,17 @@
         <evaluate expression="'proceed'" />
 
         <transition on="proceed" to="ClientStorageSave" />
-        <transition on="NoSaveNeeded" to="CompleteLogout" />
+        <transition on="NoSaveNeeded" to="#{transitionAfterDestroy}" />
     </action-state>
     
     <subflow-state id="ClientStorageSave" subflow="client-storage/write">
         <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="CompleteLogout"/>
+        <transition on="proceed" to="#{transitionAfterDestroy}"/>
     </subflow-state>
 
     <!-- Continue the logout process. -->
 
-    <action-state id="CompleteLogout">
-        <evaluate expression="LogoutPopulateAuditContext" />
+    <action-state id="ContinueLogout">
         <evaluate expression="PopulateMultiRPContextFromLogoutContext" />
         <evaluate expression="'proceed'" />
         
@@ -75,7 +90,6 @@
 
     <view-state id="LogoutView" view="logout">
         <on-render>
-            <evaluate expression="WriteAuditLog" />
             <evaluate expression="environment" result="viewScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
             <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutContext))" result="viewScope.logoutContext" />
@@ -86,14 +100,25 @@
             <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
         </on-render>
         <transition on="proceed" to="LogoutCompleteView" />
+        <transition on="local" to="DestroySessions">
+            <set name="flowScope.transitionAfterDestroy" value="'LogoutCompleteView'" />
+        </transition>
+        <transition on="propagate" to="CheckPromptCondition2">
+            <set name="flowScope.transitionAfterDestroy" value="'LogoutPropagateView'" />
+        </transition>
         <transition on="end" to="LogoutCompleteView" />
-        <transition on="propagate" to="LogoutPropagateView" />
     </view-state>
 
+    <decision-state id="CheckPromptCondition2">
+        <if test="promptForIdP"
+            then="DestroySessions" else="#{transitionAfterDestroy}" />
+    </decision-state>
+
     <!-- Terminus -->
 
     <end-state id="LogoutPropagateView" view="logout-propagate">
         <on-entry>
+            <evaluate expression="WriteAuditLog" />
             <evaluate expression="SaveLogoutContext" />
             <evaluate expression="environment" result="requestScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
@@ -111,6 +136,7 @@
 
     <end-state id="LogoutCompleteView" view="logout-complete">
         <on-entry>
+            <evaluate expression="WriteAuditLog" />
             <evaluate expression="environment" result="requestScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
             <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutContext))" result="requestScope.logoutContext" />
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 90fa64d..8fbab6b 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
@@ -46,9 +46,12 @@
 
     <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="DestroySessions"
+        class="net.shibboleth.idp.session.impl.DestroySessions" scope="prototype"
+        p:sessionManager-ref="shibboleth.SessionManager" />
+    
     <bean id="LogoutPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
         p:fieldExtractors="#{getObject('shibboleth.LogoutRequestAuditExtractors') ?: getObject('shibboleth.DefaultLogoutRequestAuditExtractors')}" />
     
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 f30f92c..f4b3802 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
@@ -19,6 +19,7 @@
     <action-state id="DoProfileWork">
         <evaluate expression="ProcessLogoutRequest" />
         <evaluate expression="LogoutPopulateAuditContext" />
+        <evaluate expression="DestroySessions" />
 
         <evaluate expression="'proceed'" />
         
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 2c1a453..fd65165 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
@@ -17,6 +17,8 @@
         
     <import resource="common-beans.xml" />
 
+    <alias alias="UserPromptCondition" name="%{idp.logout.promptUser:shibboleth.Conditions.FALSE}" />
+
     <util:constant id="shibboleth.EndpointType"
         static-field="org.opensaml.saml.saml2.metadata.SingleLogoutService.DEFAULT_ELEMENT_NAME" />
 
@@ -36,9 +38,12 @@
 
     <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="DestroySessions"
+        class="net.shibboleth.idp.session.impl.DestroySessions" scope="prototype"
+        p:sessionManager-ref="shibboleth.SessionManager" />
+    
     <bean id="LogoutPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
         p:fieldExtractors="#{getObject('shibboleth.LogoutRequestAuditExtractors') ?: getObject('shibboleth.DefaultLogoutRequestAuditExtractors')}" />
 
@@ -49,23 +54,6 @@
         <property name="fallbackLanguages">
             <bean parent="shibboleth.CommaDelimStringArray" c:_0="#{'%{idp.ui.fallbackLanguages:}'.trim()}" />
         </property>
-        <property name="metadataContextLookupStrategy">
-            <bean class="com.google.common.base.Functions" factory-method="compose">
-                <constructor-arg name="g">
-                    <bean class="com.google.common.base.Functions" factory-method="compose">
-                        <constructor-arg name="g">
-                            <ref bean="shibboleth.ChildLookup.SAMLMetadataContext" />
-                        </constructor-arg>
-                        <constructor-arg name="f">
-                            <ref bean="shibboleth.ChildLookup.SAMLPeerEntityContext" />
-                        </constructor-arg>
-                    </bean>
-                </constructor-arg>
-                <constructor-arg name="f">
-                    <ref bean="shibboleth.MessageContextLookup.Inbound" />
-                </constructor-arg>
-            </bean>
-        </property>
         <property name="RPUIContextCreateStrategy">
             <bean class="com.google.common.base.Functions" factory-method="compose">
                 <constructor-arg name="g">
@@ -137,6 +125,22 @@
         </property>
     </bean>
 
+    <bean id="AddLogoutStatusToResponse"
+            class="org.opensaml.saml.saml2.profile.impl.AddStatusToResponse" scope="prototype"
+            p:statusMessage="An error occurred.">
+        <property name="detailedErrorsCondition">
+            <bean class="net.shibboleth.idp.profile.config.logic.DetailedErrorsPredicate" />
+        </property>
+        <property name="statusCodes">
+            <list>
+                <util:constant static-field="org.opensaml.saml.saml2.core.StatusCode.SUCCESS" />
+            </list>
+        </property>
+        <property name="statusCodesLookupStrategy">
+            <bean class="net.shibboleth.idp.session.impl.LogoutStatusStrategyFunction" />
+        </property>
+    </bean>
+
     <bean id="shibboleth.PreEncodeMessageHandler"
             class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
         <property name="handlers">
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-flow.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-flow.xml
index 833fc91..791c20c 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-flow.xml
@@ -64,6 +64,22 @@
 
     <action-state id="DoLogoutRequest">
         <evaluate expression="ProcessLogoutRequest" />
+        <evaluate expression="LogoutPopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckPromptCondition1">
+            <set name="flowScope.transitionAfterDestroy" value="'ContinueLogout'" />
+            <set name="flowScope.promptForIdP" value="UserPromptCondition.apply(opensamlProfileRequestContext)" />
+        </transition>
+    </action-state>
+
+    <decision-state id="CheckPromptCondition1">
+        <if test="promptForIdP"
+            then="ContinueLogout" else="DestroySessions" />
+    </decision-state>
+
+    <action-state id="DestroySessions">
+        <evaluate expression="DestroySessions" />
         <evaluate expression="'proceed'" />
         
         <transition on="proceed" to="PopulateClientStorageSaveContext" />
@@ -76,19 +92,18 @@
         <evaluate expression="'proceed'" />
 
         <transition on="proceed" to="ClientStorageSave" />
-        <transition on="NoSaveNeeded" to="CompleteLogout" />
+        <transition on="NoSaveNeeded" to="#{transitionAfterDestroy}" />
     </action-state>
     
     <subflow-state id="ClientStorageSave" subflow="client-storage/write">
         <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="CompleteLogout"/>
+        <transition on="proceed" to="#{transitionAfterDestroy}"/>
     </subflow-state>
 
     <!-- Continue the logout process. -->
 
-    <action-state id="CompleteLogout">
+    <action-state id="ContinueLogout">
         <evaluate expression="SetIssuerRPUIInformation" />
-        <evaluate expression="LogoutPopulateAuditContext" />
         <evaluate expression="PopulateMultiRPContextFromLogoutContext" />
         <evaluate expression="'proceed'" />
         
@@ -123,10 +138,20 @@
         </on-render>
         
         <transition on="proceed" to="CheckAsync2" />
-        <transition on="propagate" to="LogoutPropagateView" />
+        <transition on="local" to="DestroySessions">
+            <set name="flowScope.transitionAfterDestroy" value="'LogoutCompleteView'" />
+        </transition>
+        <transition on="propagate" to="CheckPromptCondition2">
+            <set name="flowScope.transitionAfterDestroy" value="'LogoutPropagateView'" />
+        </transition>
         <transition on="end" to="LogoutCompleteView" />
     </view-state>
     
+    <decision-state id="CheckPromptCondition2">
+        <if test="promptForIdP"
+            then="DestroySessions" else="#{transitionAfterDestroy}" />
+    </decision-state>
+    
     <view-state id="LogoutPropagateView" view="logout-propagate">
         <on-render>
             <evaluate expression="SaveLogoutContext" />
@@ -169,6 +194,7 @@
     <action-state id="BuildResponse">
         <evaluate expression="AddLogoutResponse" />
         <evaluate expression="AddInResponseToToResponse" />
+        <evaluate expression="AddLogoutStatusToResponse" />
         <evaluate expression="PostResponsePopulateAuditContext" />
         
         <evaluate expression="'proceed'" />
diff --git a/idp-conf/src/main/resources/system/messages/messages.properties b/idp-conf/src/main/resources/system/messages/messages.properties
index 86d1986..e4f89d0 100644
--- a/idp-conf/src/main/resources/system/messages/messages.properties
+++ b/idp-conf/src/main/resources/system/messages/messages.properties
@@ -123,13 +123,23 @@ idp.userprefs.no-js = This feature requires Javascript.
 # Logout-related messages
 
 idp.logout.sp-initiated = You have been logged out of the following service:
-idp.logout.ask = Would you like to attempt to log out of all services accessed during your session? \
-                    Please select <strong>Yes</strong> or <strong>No</strong> to ensure the logout \
-                    operation completes, or wait a few seconds for Yes.
-idp.logout.contactServices = If you proceed, the system will attempt to contact the following services:
+idp.logout.prompt = Choose one of the following, or wait a few seconds for the default.
+idp.logout.idponly = Logout Locally
+idp.logout.idponly.caption = End your SSO session.
+idp.logout.global = Logout Globally
+idp.logout.global.caption = End your SSO session and attempt logout of services accessed during session.
+idp.logout.cancel = Cancel
+idp.logout.cancel.caption = Cancel logout and retain your SSO session.
+idp.logout.contactServices = If instructed, the system will attempt to contact the following services:
 idp.logout.complete = The logout operation is complete, and no other services appear to have been accessed during this session.
 idp.logout.local = You elected not to log out of all the services accessed during your session.
 idp.logout.attempt = Attempting to log out of the following services:
+idp.logout.cancelled = Logout has been cancelled.
+
+# Legacy V3 UI
+idp.logout.ask = Would you like to attempt to log out of all services accessed during your session? \
+                    Please select <strong>Yes</strong> or <strong>No</strong> to ensure the logout \
+                    operation completes, or wait a few seconds for Yes.
 
 # Messages related to attribute release consent.
 
diff --git a/idp-conf/src/main/resources/views/logout-complete.vm b/idp-conf/src/main/resources/views/logout-complete.vm
index d780252..3b0fcae 100644
--- a/idp-conf/src/main/resources/views/logout-complete.vm
+++ b/idp-conf/src/main/resources/views/logout-complete.vm
@@ -14,6 +14,8 @@
 ## environment - Spring Environment object for property resolution
 ## custom - arbitrary object injected by deployer
 ##
+#set ($activeIdPSessions = $logoutContext and !$logoutContext.getIdPSessions().isEmpty())
+#set ($activeSPSessions = $logoutContext and !$logoutContext.getSessionMap().isEmpty())
 <!DOCTYPE html>
 <html>
     <head>
@@ -32,7 +34,13 @@
 
         <div class="content">
           <div class="column one">
+          #if ($activeIdPSessions)
+            <p>#springMessageText("idp.logout.cancelled", "Logout has been cancelled.")</p>
+          #elseif ($activeSPSessions)
             <p>#springMessageText("idp.logout.local", "You elected not to log out of all the applications accessed during your session.")</p>
+          #else
+            <p>#springMessageText("idp.logout.complete", "The logout operation is complete, and no other services appear to have been accessed during this session.")</p>
+          #end
           </div>
           <div class="column two">
             <ul class="list list-help">
diff --git a/idp-conf/src/main/resources/views/logout.vm b/idp-conf/src/main/resources/views/logout.vm
index d31ae0e..e8f3cd7 100644
--- a/idp-conf/src/main/resources/views/logout.vm
+++ b/idp-conf/src/main/resources/views/logout.vm
@@ -18,20 +18,26 @@
 #if ($rpContext)
 #set ($rpUIContext = $rpContext.getSubcontext("net.shibboleth.idp.ui.context.RelyingPartyUIContext"))
 #end
+#set ($promptForIdP = $logoutContext and !$logoutContext.getIdPSessions().isEmpty())
+#set ($promptForSP = $logoutContext and !$logoutContext.getSessionMap().isEmpty())
 <!DOCTYPE html>
 <html>
     <head>
         <meta charset="utf-8">
         <meta name="viewport" content="width=device-width,initial-scale=1.0">
-        #if ( $logoutContext and !$logoutContext.getSessionMap().isEmpty() )
+        #*
+        #if ($promptForSP)
             <meta http-equiv="refresh" content="10;url=$flowExecutionUrl&_eventId=propagate">
+        #elseif ($promptForIdP)
+            <meta http-equiv="refresh" content="10;url=$flowExecutionUrl&_eventId=local">
         #end
+        *#
         <title>#springMessageText("idp.title", "Web Login Service")</title>
         <link rel="stylesheet" type="text/css" href="$request.getContextPath()/css/main.css">
     </head>
 
     <body>
-    <div class="wrapper">
+    <div class="wrapper">   
       <div class="container">
         <header>
           <img src="$request.getContextPath()#springMessage("idp.logo")" alt="#springMessageText("idp.logo.alt-text", "logo")">
@@ -45,30 +51,37 @@
             <br>
     
             #if ($rpContext)
-                <p>#springMessageText("idp.logout.sp-initiated", "You have been logged out of the following service:")</p>
-                <blockquote>
-                #if ($rpUIContext)
-                    $encoder.encodeForHTML($rpUIContext.getServiceName())
-                #else
-                    $encoder.encodeForHTML($rpContext.getRelyingPartyId())
-                #end
-                </blockquote>
-                <br>
+              <p>#springMessageText("idp.logout.sp-initiated", "You have been logged out of the following service:")</p>
+              <blockquote>
+              #if ($rpUIContext)
+                  $encoder.encodeForHTML($rpUIContext.getServiceName())
+              #else
+                  $encoder.encodeForHTML($rpContext.getRelyingPartyId())
+              #end
+              </blockquote>
+              <br>
             #end
+            
+            #if ($promptForIdP or $promptForSP)
+              <p>#springMessageText("idp.logout.prompt", "Choose one of the following, or wait a few seconds for the default.")</p>
+              <br>
 
-            #if ( $logoutContext and !$logoutContext.getSessionMap().isEmpty() )
-                <p>#springMessageText("idp.logout.ask", "Would you like to attempt to log out of all services accessed during your session? Please select <strong>Yes</strong> or <strong>No</strong> to ensure the logout operation completes, or wait a few seconds for Yes.")</p>
-                <br>
-
-                <form id="propagate_form" method="POST" action="$flowExecutionUrl">
-                    <button id="propagate_yes" type="submit" name="_eventId" value="propagate">Yes</button>
-                    <button id="propagate_no" type="submit" name="_eventId" value="end">No</button>
-                </form>
+              <form id="propagate_form" method="POST" action="$flowExecutionUrl">
+                
+                <div class="form-element-wrapper">
+                  <button id="logout_local" class="form-element form-button" type="submit" name="_eventId" value="local">#springMessageText("idp.logout.idponly", "Logout Locally")</button>
+                  <p>#springMessageText("idp.logout.idponly.caption", "End your SSO session.")</p>
+                </div>
+            #end
 
-                <br>
-                <p>#springMessageText("idp.logout.contactServices", "If you proceed, the system will attempt to contact the following services:")</p>
-                <ol>
-                #foreach ($sp in $logoutContext.getSessionMap().keySet())
+            #if ($promptForSP)
+                <div class="form-element-wrapper">
+                  <button id="logout_propagate" class="form-element form-button" type="submit" name="_eventId" value="propagate">#springMessageText("idp.logout.global", "Logout Globally")</button>
+                  <p>#springMessageText("idp.logout.global.caption", "End your SSO session and attempt logout of services accessed during session.")</p>
+                  <br>
+                  <p>#springMessageText("idp.logout.contactServices", "If instructed, the system will attempt to contact the following services:")</p>
+                  <ol>
+                  #foreach ($sp in $logoutContext.getSessionMap().keySet())
                     #set ($rpCtx = $multiRPContext.getRelyingPartyContextById($sp))
                     #if ($rpCtx)
                       #set ($rpUIContext = $rpCtx.getSubcontext("net.shibboleth.idp.ui.context.RelyingPartyUIContext"))
@@ -78,8 +91,21 @@
                     #else
                       <li>$encoder.encodeForHTML($sp)</li>
                     #end
-                #end
-                </ol>
+                  #end
+                  </ol>
+                  <br>
+                </div>
+            #end
+
+            #if ($promptForIdP)
+                <div class="form-element-wrapper">
+                  <button id="logout_cancel" class="form-element form-button" type="submit" name="_eventId" value="end">#springMessageText("idp.logout.cancel", "Cancel")</button>
+                  <p>#springMessageText("idp.logout.cancel.caption", "Cancel logout and retain your SSO session.")</p>
+                </div>
+            #end
+            
+            #if ($promptForIdP or $promptForSP)
+              </form>
             #else
                 <p><strong>#springMessageText("idp.logout.complete", "The logout operation is complete, and no other services appear to have been accessed during this session.")</strong></p>
                 <!-- Complete the flow by adding a hidden iframe. -->
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java
index f478ccd..11da9e3 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java
@@ -34,8 +34,6 @@ import net.shibboleth.idp.saml.profile.config.navigate.QualifiedNameIDFormatsLoo
 import net.shibboleth.idp.saml.session.SAML2SPSession;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.SPSession;
-import net.shibboleth.idp.session.SessionException;
-import net.shibboleth.idp.session.SessionManager;
 import net.shibboleth.idp.session.SessionResolver;
 import net.shibboleth.idp.session.context.LogoutContext;
 import net.shibboleth.idp.session.context.SessionContext;
@@ -82,8 +80,8 @@ import com.google.common.base.Predicates;
  * @event {@link EventIds#INVALID_MESSAGE}
  * @event {@link EventIds#IO_ERROR}
  * @event {@link SAMLEventIds#SESSION_NOT_FOUND}
- * @post The matching session(s) are destroyed.
- * @post If a {@link IdPSession} was found, then a {@link SubjectContext} and {@link LogoutContext} will be populated.
+ * @post If at least one {@link IdPSession} was found, then a {@link SubjectContext} and {@link LogoutContext}
+ *  will be populated.
  * @post If a single {@link IdPSession} was found, then a {@link SessionContext} will be populated.
  */
 public class ProcessLogoutRequest extends AbstractProfileAction {
@@ -94,9 +92,6 @@ public class ProcessLogoutRequest extends AbstractProfileAction {
     /** Session resolver. */
     @NonnullAfterInit private SessionResolver sessionResolver;
 
-    /** Session manager. */
-    @NonnullAfterInit private SessionManager sessionManager;
-    
     /** Creation/lookup function for SubjectContext. */
     @Nonnull private Function<ProfileRequestContext,SubjectContext> subjectContextCreationStrategy;
 
@@ -172,17 +167,6 @@ public class ProcessLogoutRequest extends AbstractProfileAction {
         
         sessionResolver = Constraint.isNotNull(resolver, "SessionResolver cannot be null");
     }
-
-    /**
-     * Set the {@link SessionManager} to use.
-     * 
-     * @param manager  session manager to use
-     */
-    public void setSessionManager(@Nonnull final SessionManager manager) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        sessionManager = Constraint.isNotNull(manager, "SessionManager cannot be null");
-    }
     
     /**
      * Set the creation/lookup strategy for the {@link SubjectContext} to populate.
@@ -297,8 +281,6 @@ public class ProcessLogoutRequest extends AbstractProfileAction {
         if (!getActivationCondition().equals(Predicates.alwaysFalse())) {
             if (sessionResolver == null) {
                 throw new ComponentInitializationException("SessionResolver cannot be null");
-            } else if (sessionManager == null) {
-                throw new ComponentInitializationException("SessionManager cannot be null");
             }
         }
     }
@@ -344,7 +326,6 @@ public class ProcessLogoutRequest extends AbstractProfileAction {
                     sessionResolver.resolve(sessionResolverCriteriaStrategy.apply(profileRequestContext));
             final Iterator<IdPSession> sessionIterator = sessions.iterator();
 
-            IdPSession single = null;
             LogoutContext logoutCtx = null;
             
             int count = 1;
@@ -372,10 +353,9 @@ public class ProcessLogoutRequest extends AbstractProfileAction {
                     if (subjectCtx != null) {
                         subjectCtx.setPrincipalName(session.getPrincipalName());
                     }
-                    single = session;
-                } else {
-                    single = null;
                 }
+
+                logoutCtx.getIdPSessions().add(session);
                 
                 for (final SPSession spSession : session.getSPSessions()) {
                     if (!sessionMatches(profileRequestContext, spSession)) {
@@ -383,26 +363,18 @@ public class ProcessLogoutRequest extends AbstractProfileAction {
                         logoutCtx.getKeyedSessionMap().put(Integer.toString(count++), spSession);
                     }
                 }
-
-                try {
-                    sessionManager.destroySession(session.getId(), true);
-                } catch (final SessionException e) {
-                    log.error("{} Error destroying session {}", getLogPrefix(), session.getId(), e);
-                    ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
-                    return;
-                }
             }
             
-            if (single != null) {
+            if (logoutCtx == null) {
+                log.info("{} No active session(s) found matching LogoutRequest", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, SAMLEventIds.SESSION_NOT_FOUND);
+            } else if (logoutCtx.getIdPSessions().size() == 1) {
                 final SessionContext sessionCtx = sessionContextCreationStrategy.apply(profileRequestContext);
                 if (sessionCtx != null) {
-                    sessionCtx.setIdPSession(single);
+                    sessionCtx.setIdPSession(logoutCtx.getIdPSessions().iterator().next());
                 }
-            } else if (logoutCtx == null) {
-                log.info("{} No active session(s) found matching LogoutRequest", getLogPrefix());
-                ActionSupport.buildEvent(profileRequestContext, SAMLEventIds.SESSION_NOT_FOUND);
             }
-            
+
         } catch (final ResolverException e) {
             log.error("{} Error resolving matching session(s)", getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, SAMLEventIds.SESSION_NOT_FOUND);
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
index 158fe8a..8c1ad30 100644
--- a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
@@ -90,7 +90,6 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         
         action = new ProcessLogoutRequest();
         action.setSessionResolver(sessionManager);
-        action.setSessionManager(sessionManager);
         action.initialize();
     }
     
@@ -211,9 +210,12 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         Assert.assertEquals(session.getId(), sessionCtx.getIdPSession().getId());
         
         final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
-        if (logoutCtx != null) {
-            Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
-        }
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 1);
+        Assert.assertSame(logoutCtx.getIdPSessions().iterator().next(), sessionCtx.getIdPSession());
+        Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
+
+        sessionManager.destroySession(session.getId(), false);
     }
 
     @Test public void testDefaultedSessionQualifiers() throws SessionException, ResolverException {
@@ -247,9 +249,12 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         Assert.assertEquals(session.getId(), sessionCtx.getIdPSession().getId());
         
         final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
-        if (logoutCtx != null) {
-            Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
-        }
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 1);
+        Assert.assertSame(logoutCtx.getIdPSessions().iterator().next(), sessionCtx.getIdPSession());
+        Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
+        
+        sessionManager.destroySession(session.getId(), false);
     }
     
     @Test public void testSessionOneSPSession() throws SessionException, ResolverException {
@@ -280,9 +285,12 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         Assert.assertEquals(session.getId(), sessionCtx.getIdPSession().getId());
         
         final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
-        if (logoutCtx != null) {
-            Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
-        }
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 1);
+        Assert.assertSame(logoutCtx.getIdPSessions().iterator().next(), sessionCtx.getIdPSession());
+        Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
+        
+        sessionManager.destroySession(session.getId(), false);
     }
     
     @Test public void testSessionTwoSPSessions() throws SessionException, ResolverException {
@@ -316,6 +324,9 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         Assert.assertEquals(session.getId(), sessionCtx.getIdPSession().getId());
         
         final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class, true);
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 1);
+        Assert.assertSame(logoutCtx.getIdPSessions().iterator().next(), sessionCtx.getIdPSession());
         Assert.assertEquals(logoutCtx.getSessionMap().size(), 1);
         
         final SAML2SPSession sp = (SAML2SPSession) logoutCtx.getSessions(ActionTestingSupport.INBOUND_MSG_ISSUER + "/2").iterator().next();
@@ -324,6 +335,8 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         Assert.assertEquals(sp.getExpirationInstant(), expiration);
         Assert.assertTrue(SAML2ObjectSupport.areNameIDsEquivalent(nameId2, sp.getNameID()));
         Assert.assertEquals(sp.getSessionIndex(), "index2");
+        
+        sessionManager.destroySession(session.getId(), false);
     }
 
     @Test public void testTwoSPSessionsWrongRequester() throws SessionException, ResolverException {
@@ -395,10 +408,12 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         Assert.assertEquals(session.getId(), sessionCtx.getIdPSession().getId());
         
         final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class, false);
-        if (logoutCtx != null) {
-            Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
-        }
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 1);
+        Assert.assertSame(logoutCtx.getIdPSessions().iterator().next(), sessionCtx.getIdPSession());
+        Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
         
+        sessionManager.destroySession(session.getId(), false);
         sessionManager.destroySession(session2.getId(), false);
     }
 
@@ -438,9 +453,12 @@ public class ProcessLogoutRequestTest extends SessionManagerBaseTestCase {
         Assert.assertNull(sessionCtx);
         
         final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
-        if (logoutCtx != null) {
-            Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
-        }
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 2);
+        Assert.assertEquals(logoutCtx.getSessionMap().size(), 0);
+
+        sessionManager.destroySession(session.getId(), false);
+        sessionManager.destroySession(session2.getId(), false);
     }
     
 }
\ No newline at end of file
diff --git a/idp-session-api/src/main/java/net/shibboleth/idp/session/context/LogoutContext.java b/idp-session-api/src/main/java/net/shibboleth/idp/session/context/LogoutContext.java
index cc6b554..a5c8580 100644
--- a/idp-session-api/src/main/java/net/shibboleth/idp/session/context/LogoutContext.java
+++ b/idp-session-api/src/main/java/net/shibboleth/idp/session/context/LogoutContext.java
@@ -17,12 +17,14 @@
 
 package net.shibboleth.idp.session.context;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 
+import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.SPSession;
 import net.shibboleth.utilities.java.support.annotation.constraint.Live;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
@@ -40,17 +42,32 @@ import com.google.common.collect.Multimap;
  */
 public final class LogoutContext extends BaseContext {
 
+    /** Primary sessions to destroy. */
+    @Nonnull @NonnullElements private final Collection<IdPSession> idpSessions;
+
     /** SP sessions needing logout. */
     @Nonnull @NonnullElements private final Multimap<String,SPSession> sessionMap;
 
     /** An index of the session objects by an externally assigned key. */
     @Nonnull @NonnullElements private final Map<String,SPSession> keyedSessionMap;
-    
+        
     /** Constructor. */
     public LogoutContext() {
+        idpSessions = new ArrayList<>();
         sessionMap = ArrayListMultimap.create(10, 1);
         keyedSessionMap = new HashMap<>();
     }
+    
+    /**
+     * Get a live collection of the IdP Sessions being destroyed.
+     * 
+     * @return sessions being destroyed
+     * 
+     * @since 4.0.0
+     */
+    @Nonnull @NonnullElements @Live public Collection<IdPSession> getIdPSessions() {
+        return idpSessions;
+    }
 
     /**
      * Get a live view of the map of service ID/session mappings.
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/DestroySessions.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/DestroySessions.java
new file mode 100644
index 0000000..6a82ddf
--- /dev/null
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/DestroySessions.java
@@ -0,0 +1,178 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.SessionException;
+import net.shibboleth.idp.session.SessionManager;
+import net.shibboleth.idp.session.context.LogoutContext;
+import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+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.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;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+
+/**
+ * Profile action that destroys any {@link IdPSession}s found in a {@link LogoutContext}.
+ * 
+ * <p>If a {@link SessionContext} is found, the corresponding session is also unbound
+ * from the client and the {@link SessionContext} is removed.</p>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#IO_ERROR}
+ * @post The sessions are removed from the session manager.
+ * @post The sessions are removed from the {@LogoutContext}.
+ * @post The {@SessionContext} is removed if it matched one of the sessions destroyed.
+ * 
+ * @since 4.0.0
+ */
+public class DestroySessions extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DestroySessions.class);
+    
+    /** Session resolver. */
+    @NonnullAfterInit private SessionManager sessionManager;
+
+    /** Lookup function for SessionContext. */
+    @Nonnull private Function<ProfileRequestContext,SessionContext> sessionContextLookupStrategy;
+
+    /** Lookup function for LogoutContext. */
+    @Nonnull private Function<ProfileRequestContext,LogoutContext> logoutContextLookupStrategy;
+    
+    /** SessionContext to access. */
+    @Nullable private SessionContext sessionContext;
+
+    /** LogoutContext to access. */
+    @Nullable private LogoutContext logoutContext;
+    
+    /** Constructor. */
+    public DestroySessions() {
+        sessionContextLookupStrategy = new ChildContextLookup<>(SessionContext.class);
+        logoutContextLookupStrategy = new ChildContextLookup<>(LogoutContext.class);
+    }
+    
+    /**
+     * Set the {@link SessionManager} to use.
+     * 
+     * @param manager  session manager to use
+     */
+    public void setSessionManager(@Nonnull final SessionManager manager) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        sessionManager = Constraint.isNotNull(manager, "SessionManager cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy for the SessionContext to access.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setSessionContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SessionContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        sessionContextLookupStrategy = Constraint.isNotNull(strategy,
+                "SessionContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy for the LogoutContext to access.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setLogoutContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,LogoutContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        logoutContextLookupStrategy = Constraint.isNotNull(strategy,
+                "LogoutContext lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (!getActivationCondition().equals(Predicates.alwaysFalse())) {
+            if (sessionManager == null) {
+                throw new ComponentInitializationException("SessionManager cannot be null");
+            }
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        logoutContext = logoutContextLookupStrategy.apply(profileRequestContext);
+        if (logoutContext == null || logoutContext.getIdPSessions().isEmpty()) {
+            log.debug("{} No LogoutContext or IdPSessions found, nothing to do", getLogPrefix());
+            return false;
+        }
+        
+        sessionContext = sessionContextLookupStrategy.apply(profileRequestContext);
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        for (final IdPSession session : logoutContext.getIdPSessions()) {
+            log.debug("{} Attempting destruction of session {}", getLogPrefix(), session.getId());
+            
+            final boolean unbind = sessionContext != null && sessionContext.getIdPSession() != null
+                    ? sessionContext.getIdPSession().equals(session)
+                            : false;
+            if (unbind) {
+                sessionContext.getParent().removeSubcontext(sessionContext);
+                sessionContext = null;
+            }
+            
+            try {
+                sessionManager.destroySession(session.getId(), unbind);
+            } catch (final SessionException e) {
+                log.error("{} Error destroying session", getLogPrefix(), e);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            }
+        }
+        
+        logoutContext.getIdPSessions().clear();
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/LogoutStatusStrategyFunction.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/LogoutStatusStrategyFunction.java
new file mode 100644
index 0000000..d0d1f6e
--- /dev/null
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/LogoutStatusStrategyFunction.java
@@ -0,0 +1,78 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.StatusCode;
+
+import com.google.common.base.Function;
+
+import net.shibboleth.idp.session.context.LogoutContext;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A strategy function for determining the status of a logout based on the content of
+ * a {@link LogoutContext}.
+ * 
+ * <p>It signals an error if the context still contains any {@link IdPSession} objects,
+ * indicating a logout was cancelled.</p>
+ */
+public class LogoutStatusStrategyFunction implements Function<ProfileRequestContext,List<String>> {
+    
+    /** Lookup strategy for context. */
+    @Nonnull private Function<ProfileRequestContext,LogoutContext> logoutContextLookupStrategy;
+    
+    /** Constructor. */
+    public LogoutStatusStrategyFunction() {
+        logoutContextLookupStrategy = new ChildContextLookup<>(LogoutContext.class);
+    }
+
+    /**
+     * Set the lookup strategy for the LogoutContext to access.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setLogoutContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,LogoutContext> strategy) {
+        
+        logoutContextLookupStrategy = Constraint.isNotNull(strategy,
+                "LogoutContext lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public @Nullable List<String> apply(@Nullable final ProfileRequestContext input) {
+        
+        final LogoutContext logoutCtx = logoutContextLookupStrategy.apply(input);
+        if (logoutCtx != null) {
+            if (!logoutCtx.getIdPSessions().isEmpty()) {
+                return Arrays.asList(StatusCode.RESPONDER, StatusCode.REQUEST_DENIED);
+            }
+        }
+        
+        return Collections.emptyList();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ProcessLogout.java b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ProcessLogout.java
index 743df36..64f879e 100644
--- a/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ProcessLogout.java
+++ b/idp-session-impl/src/main/java/net/shibboleth/idp/session/impl/ProcessLogout.java
@@ -25,7 +25,6 @@ import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.SPSession;
 import net.shibboleth.idp.session.SessionException;
-import net.shibboleth.idp.session.SessionManager;
 import net.shibboleth.idp.session.SessionResolver;
 import net.shibboleth.idp.session.context.LogoutContext;
 import net.shibboleth.idp.session.context.SessionContext;
@@ -49,7 +48,7 @@ import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
 
 /**
- * Profile action that resolves an active session from the profile request, and destroys it,
+ * Profile action that resolves an active session from the profile request, and records it,
  * populating the associated {@link SPSession} objects into a {@link LogoutContext}.
  * 
  * <p>A {@link SubjectContext} and {@link SessionContext} are also populated.</p>
@@ -60,7 +59,6 @@ import com.google.common.base.Predicates;
  * @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#IO_ERROR}
- * @post The matching {@link IdPSession} is destroyed.
  * @post If a {@link IdPSession} was found, then a {@link SubjectContext} and {@link LogoutContext} will be populated.
  */
 public class ProcessLogout extends AbstractProfileAction {
@@ -71,9 +69,6 @@ public class ProcessLogout extends AbstractProfileAction {
     /** Session resolver. */
     @NonnullAfterInit private SessionResolver sessionResolver;
 
-    /** Session manager. */
-    @NonnullAfterInit private SessionManager sessionManager;
-
     /** Condition to determine whether to enforce address binding on the session. */
     @Nonnull private Predicate<ProfileRequestContext> checkAddressCondition;
 
@@ -129,17 +124,6 @@ public class ProcessLogout extends AbstractProfileAction {
         
         sessionResolver = Constraint.isNotNull(resolver, "SessionResolver cannot be null");
     }
-
-    /**
-     * Set the {@link SessionManager} to use.
-     * 
-     * @param manager  session manager to use
-     */
-    public void setSessionManager(@Nonnull final SessionManager manager) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        sessionManager = Constraint.isNotNull(manager, "SessionManager cannot be null");
-    }
     
     /**
      * Set the creation/lookup strategy for the SubjectContext to populate.
@@ -199,8 +183,6 @@ public class ProcessLogout extends AbstractProfileAction {
         if (!getActivationCondition().equals(Predicates.alwaysFalse())) {
             if (sessionResolver == null) {
                 throw new ComponentInitializationException("SessionResolver cannot be null");
-            } else if (sessionManager == null) {
-                throw new ComponentInitializationException("SessionManager cannot be null");
             }
         }
     }
@@ -254,18 +236,14 @@ public class ProcessLogout extends AbstractProfileAction {
                 return;
             }
             
+            logoutCtx.getIdPSessions().add(session);
+            
             int count = 1;
             for (final SPSession spSession : session.getSPSessions()) {
                 logoutCtx.getSessionMap().put(spSession.getId(), spSession);
                 logoutCtx.getKeyedSessionMap().put(Integer.toString(count++), spSession);
             }
                 
-            try {
-                sessionManager.destroySession(session.getId(), true);
-            } catch (final SessionException e) {
-                log.error("{} Error destroying session", getLogPrefix(), e);
-                ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
-            }
         } catch (final ResolverException e) {
             log.error("{} Error resolving matching session(s)", getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/DestroySessionsTest.java
similarity index 55%
copy from idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
copy to idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/DestroySessionsTest.java
index 78f7bfe..fcaabc4 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/DestroySessionsTest.java
@@ -21,7 +21,6 @@ import java.util.Collections;
 
 import javax.servlet.http.Cookie;
 
-import net.shibboleth.idp.authn.context.SubjectContext;
 import net.shibboleth.idp.profile.ActionTestingSupport;
 import net.shibboleth.idp.profile.RequestContextBuilder;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
@@ -31,7 +30,9 @@ import net.shibboleth.idp.session.SPSession;
 import net.shibboleth.idp.session.SPSessionSerializerRegistry;
 import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.idp.session.context.LogoutContext;
+import net.shibboleth.idp.session.context.SessionContext;
 import net.shibboleth.idp.session.criterion.HttpServletRequestCriterion;
+import net.shibboleth.idp.session.criterion.SessionIdCriterion;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.net.HttpServletRequestResponseContext;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
@@ -47,23 +48,20 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-/** {@link ProcessLogout} unit test. */
-public class ProcessLogoutTest extends SessionManagerBaseTestCase {
+/** {@link DestroySessions} unit test. */
+public class DestroySessionsTest extends SessionManagerBaseTestCase {
     
     private RequestContext src;
     
     private ProfileRequestContext prc;
     
-    private ProcessLogout action;
+    private DestroySessions action;
     
     @BeforeMethod public void setUpAction() throws ComponentInitializationException {
         src = new RequestContextBuilder().buildRequestContext();
         prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
         
-        action = new ProcessLogout();
-        action.setHttpServletRequest(requestProxy);
-        action.setHttpServletResponse(responseProxy);
-        action.setSessionResolver(sessionManager);
+        action = new DestroySessions();
         action.setSessionManager(sessionManager);
         action.initialize();
     }
@@ -82,86 +80,131 @@ public class ProcessLogoutTest extends SessionManagerBaseTestCase {
         sessionManager.setSPSessionSerializerRegistry(registry);
     }
     
-    @Test public void testNoSession() {
-        HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
+    @Test public void testNoContext() {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+    }
+
+    @Test public void testNoSessions() {
+        prc.getSubcontext(LogoutContext.class, true);
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
-        Assert.assertNull(prc.getSubcontext(SubjectContext.class));
-        Assert.assertNull(prc.getSubcontext(LogoutContext.class));
     }
     
-    @Test public void testSessionNoSPSessions() throws SessionException {
+    @Test public void testOneSessionNoUnbind() throws SessionException, ResolverException {
         final Cookie cookie = createSession("joe");
         
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
+
+        IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        Assert.assertNotNull(session);
+        final String sessionId = session.getId();
         
+        prc.getSubcontext(LogoutContext.class, true).getIdPSessions().add(session);
+
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
-        final SubjectContext subjectCtx = prc.getSubcontext(SubjectContext.class);
-        Assert.assertNotNull(subjectCtx);
-        Assert.assertEquals(subjectCtx.getPrincipalName(), "joe");
-        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class, true);
-        Assert.assertTrue(logoutCtx.getSessionMap().isEmpty());
+        
+        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertTrue(logoutCtx.getIdPSessions().isEmpty());
+        
+        session = sessionManager.resolveSingle(new CriteriaSet(new SessionIdCriterion(sessionId)));
+        Assert.assertNull(session);
     }
 
-    @Test public void testSessionSPSessions() throws SessionException, ResolverException {
+    @Test public void testOneSessionUnbind() throws SessionException, ResolverException {
         final Cookie cookie = createSession("joe");
         
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
 
-        final long creation = System.currentTimeMillis();
-        final long expiration = creation + 3600 * 60 * 1000;
-
-        final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
         Assert.assertNotNull(session);
-        session.addSPSession(new BasicSPSession("https://sp.example.org", creation, expiration));
-        session.addSPSession(new BasicSPSession("https://sp2.example.org", creation, expiration));
+        final String sessionId = session.getId();
+        
+        prc.getSubcontext(LogoutContext.class, true).getIdPSessions().add(session);
+        prc.getSubcontext(SessionContext.class, true).setIdPSession(session);
         
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
-        final SubjectContext subjectCtx = prc.getSubcontext(SubjectContext.class);
-        Assert.assertNotNull(subjectCtx);
-        Assert.assertEquals(subjectCtx.getPrincipalName(), "joe");
-        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class, true);
-        
-        BasicSPSession sp = (BasicSPSession) logoutCtx.getSessions("https://sp.example.org").iterator().next();
-        Assert.assertNotNull(sp);
-        Assert.assertEquals(sp.getCreationInstant(), creation);
-        Assert.assertEquals(sp.getExpirationInstant(), expiration);
-
-        sp = (BasicSPSession) logoutCtx.getSessions("https://sp2.example.org").iterator().next();
-        Assert.assertNotNull(sp);
-        Assert.assertEquals(sp.getCreationInstant(), creation);
-        Assert.assertEquals(sp.getExpirationInstant(), expiration);
-}
-    
-    @Test public void testAddressRebind() throws SessionException {
-        final Cookie cookie = createSession("joe");
         
+        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertTrue(logoutCtx.getIdPSessions().isEmpty());
+        
+        final SessionContext sessionCtx = prc.getSubcontext(SessionContext.class);
+        Assert.assertNull(sessionCtx);
+        
+        session = sessionManager.resolveSingle(new CriteriaSet(new SessionIdCriterion(sessionId)));
+        Assert.assertNull(session);
+    }
+
+    @Test public void testOneSessionDifferent() throws SessionException, ResolverException {
+        Cookie cookie = createSession("joe");
+        
+        HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
+        ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
+
+        IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        Assert.assertNotNull(session);
+
+        prc.getSubcontext(LogoutContext.class, true).getIdPSessions().add(session);
+
+        cookie = createSession("joe");
+
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
-        ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setRemoteAddr("::1");
+
+        session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        Assert.assertNotNull(session);
+                
+        prc.getSubcontext(SessionContext.class, true).setIdPSession(session);
         
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
-        final SubjectContext subjectCtx = prc.getSubcontext(SubjectContext.class);
-        Assert.assertNotNull(subjectCtx);
-        Assert.assertEquals(subjectCtx.getPrincipalName(), "joe");
+        
+        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertTrue(logoutCtx.getIdPSessions().isEmpty());
+        
+        final SessionContext sessionCtx = prc.getSubcontext(SessionContext.class);
+        Assert.assertNotNull(sessionCtx);
+        Assert.assertNotNull(sessionCtx.getIdPSession());
     }
-    
-    @Test public void testAddressMismatch() throws SessionException {
-        final Cookie cookie = createSession("joe");
+
+    @Test public void testTwoSessions() throws SessionException, ResolverException {
+        Cookie cookie = createSession("joe");
         
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
-        ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setRemoteAddr("192.168.1.1");
+
+        IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        Assert.assertNotNull(session);
+
+        prc.getSubcontext(LogoutContext.class, true).getIdPSessions().add(session);
+
+        cookie = createSession("joe");
+
+        HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
+        ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
+
+        session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        Assert.assertNotNull(session);
+                
+        prc.getSubcontext(LogoutContext.class, true).getIdPSessions().add(session);
+        prc.getSubcontext(SessionContext.class, true).setIdPSession(session);
         
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
-        Assert.assertNull(prc.getSubcontext(SubjectContext.class));
-        Assert.assertNull(prc.getSubcontext(LogoutContext.class));
+        
+        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertTrue(logoutCtx.getIdPSessions().isEmpty());
+        
+        final SessionContext sessionCtx = prc.getSubcontext(SessionContext.class);
+        Assert.assertNull(sessionCtx);
     }
 
 }
\ No newline at end of file
diff --git a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
index 78f7bfe..ceddf98 100644
--- a/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
+++ b/idp-session-impl/src/test/java/net/shibboleth/idp/session/impl/ProcessLogoutTest.java
@@ -64,7 +64,6 @@ public class ProcessLogoutTest extends SessionManagerBaseTestCase {
         action.setHttpServletRequest(requestProxy);
         action.setHttpServletResponse(responseProxy);
         action.setSessionResolver(sessionManager);
-        action.setSessionManager(sessionManager);
         action.initialize();
     }
     
@@ -90,19 +89,27 @@ public class ProcessLogoutTest extends SessionManagerBaseTestCase {
         Assert.assertNull(prc.getSubcontext(LogoutContext.class));
     }
     
-    @Test public void testSessionNoSPSessions() throws SessionException {
+    @Test public void testSessionNoSPSessions() throws SessionException, ResolverException {
         final Cookie cookie = createSession("joe");
         
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
-        
+
+        final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        Assert.assertNotNull(session);
+
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
         final SubjectContext subjectCtx = prc.getSubcontext(SubjectContext.class);
         Assert.assertNotNull(subjectCtx);
         Assert.assertEquals(subjectCtx.getPrincipalName(), "joe");
-        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class, true);
+        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 1);
+        Assert.assertEquals(logoutCtx.getIdPSessions().iterator().next().getId(), session.getId());
         Assert.assertTrue(logoutCtx.getSessionMap().isEmpty());
+        
+        sessionManager.destroySession(session.getId(), false);
     }
 
     @Test public void testSessionSPSessions() throws SessionException, ResolverException {
@@ -124,7 +131,10 @@ public class ProcessLogoutTest extends SessionManagerBaseTestCase {
         final SubjectContext subjectCtx = prc.getSubcontext(SubjectContext.class);
         Assert.assertNotNull(subjectCtx);
         Assert.assertEquals(subjectCtx.getPrincipalName(), "joe");
-        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class, true);
+        final LogoutContext logoutCtx = prc.getSubcontext(LogoutContext.class);
+        Assert.assertNotNull(logoutCtx);
+        Assert.assertEquals(logoutCtx.getIdPSessions().size(), 1);
+        Assert.assertEquals(logoutCtx.getIdPSessions().iterator().next().getId(), session.getId());
         
         BasicSPSession sp = (BasicSPSession) logoutCtx.getSessions("https://sp.example.org").iterator().next();
         Assert.assertNotNull(sp);
@@ -135,20 +145,27 @@ public class ProcessLogoutTest extends SessionManagerBaseTestCase {
         Assert.assertNotNull(sp);
         Assert.assertEquals(sp.getCreationInstant(), creation);
         Assert.assertEquals(sp.getExpirationInstant(), expiration);
+        
+        sessionManager.destroySession(session.getId(), false);
 }
     
-    @Test public void testAddressRebind() throws SessionException {
+    @Test public void testAddressRebind() throws SessionException, ResolverException {
         final Cookie cookie = createSession("joe");
         
         HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setCookies(cookie);
         ((MockHttpServletRequest) HttpServletRequestResponseContext.getRequest()).setRemoteAddr("::1");
         
+        final IdPSession session = sessionManager.resolveSingle(new CriteriaSet(new HttpServletRequestCriterion()));
+        Assert.assertNotNull(session);
+        
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
         final SubjectContext subjectCtx = prc.getSubcontext(SubjectContext.class);
         Assert.assertNotNull(subjectCtx);
         Assert.assertEquals(subjectCtx.getPrincipalName(), "joe");
+        
+        sessionManager.destroySession(session.getId(), false);
     }
     
     @Test public void testAddressMismatch() throws SessionException {

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


More information about the commits mailing list