[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