Merge remote branch 'tags/2.3.4' master v2.3.4
authorTamas Frank <sitya@niif.hu>
Thu, 27 Oct 2011 18:08:54 +0000 (20:08 +0200)
committerTamas Frank <sitya@niif.hu>
Thu, 27 Oct 2011 18:08:54 +0000 (20:08 +0200)
Conflicts:
pom.xml
src/installer/resources/conf-tmpl/handler.xml
src/installer/resources/conf-tmpl/relying-party.xml

29 files changed:
pom.xml
src/installer/resources/conf-tmpl/handler.xml
src/installer/resources/conf-tmpl/relying-party.xml
src/installer/resources/conf-tmpl/tc-config.xml [new file with mode: 0644]
src/installer/resources/metadata-tmpl/idp-metadata.xml
src/main/java/edu/internet2/middleware/shibboleth/idp/config/profile/ProfileHandlerNamespaceHandler.java
src/main/java/edu/internet2/middleware/shibboleth/idp/config/profile/saml2/SAML2SLOProfileHandlerBeanDefinitionParser.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml1/ShibbolethSSOProfileHandler.java
src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/SLOProfileHandler.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/SSOProfileHandler.java
src/main/java/edu/internet2/middleware/shibboleth/idp/session/ServiceInformation.java
src/main/java/edu/internet2/middleware/shibboleth/idp/session/impl/ServiceInformationImpl.java
src/main/java/edu/internet2/middleware/shibboleth/idp/session/impl/SessionManagerImpl.java
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/HTTPClientInTransportAdapter.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/HTTPClientOutTransportAdapter.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/LogoutServlet.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SLOContextFilter.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SLOServlet.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContext.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContextEntry.java [new file with mode: 0644]
src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContextStorageHelper.java [new file with mode: 0644]
src/main/resources/schema/shibboleth-2.0-idp-profile-handler.xsd
src/main/webapp/WEB-INF/web.xml
src/main/webapp/css/main.css [new file with mode: 0644]
src/main/webapp/images/failed.png [new file with mode: 0644]
src/main/webapp/images/indicator.gif [new file with mode: 0644]
src/main/webapp/images/success.png [new file with mode: 0644]
src/main/webapp/sloController.jsp [new file with mode: 0644]
src/main/webapp/sloQuestion.jsp [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index d1eeede..e217e1c 100644 (file)
--- a/pom.xml
+++ b/pom.xml
             <artifactId>xmlunit</artifactId>
         </dependency>
     </dependencies>
+    <distributionManagement>
+        <repository>
+            <id>release</id>
+            <url>${dist.release.url}</url>
+        </repository>
+        <snapshotRepository>
+            <id>snapshot</id>
+            <url>${dist.release.url}</url>
+        </snapshotRepository>
+    </distributionManagement>
 
     <build>
         <plugins>
         </profile>
     </profiles>
 
+    <!-- Project Metadata -->
+    <url>http://shibboleth.internet2.edu/</url>
+
+    <inceptionYear>2006</inceptionYear>
+
+    <licenses>
+        <license>
+            <name>Apache 2</name>
+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+            <distribution>repo</distribution>
+        </license>
+    </licenses>
+
+    <organization>
+        <name>Internet2</name>
+        <url>http://www.internet2.edu/</url>
+    </organization>
+
+    <issueManagement>
+        <system>JIRA</system>
+        <url>http://bugs.internet2.edu/</url>
+    </issueManagement>
+
+    <mailingLists>
+        <mailingList>
+            <name>Shibboleth Announce</name>
+            <subscribe>http://shibboleth.internet2.edu/support.html#lists</subscribe>
+            <unsubscribe>http://shibboleth.internet2.edu/support.html#lists</unsubscribe>
+            <post>shibboleth-announce@internet2.edu</post>
+            <archive>https://mail.internet2.edu/wws/arc/shibboleth-announce</archive>
+        </mailingList>
+        <mailingList>
+            <name>Shibboleth Users</name>
+            <subscribe>http://shibboleth.internet2.edu/support.html#lists</subscribe>
+            <unsubscribe>http://shibboleth.internet2.edu/support.html#lists</unsubscribe>
+            <post>shibboleth-users@internet2.edu</post>
+            <archive>https://mail.internet2.edu/wws/arc/shibboleth-users</archive>
+        </mailingList>
+        <mailingList>
+            <name>Shibboleth Development</name>
+            <subscribe>http://shibboleth.internet2.edu/support.html#lists</subscribe>
+            <unsubscribe>http://shibboleth.internet2.edu/support.html#lists</unsubscribe>
+            <post>shibboleth-dev@internet2.edu</post>
+            <archive>https://mail.internet2.edu/wws/arc/shibboleth-dev</archive>
+        </mailingList>
+    </mailingLists>
+
+    <scm>
+        <connection>scm:svn:https://svn.middleware.georgetown.edu/java-idp/</connection>
+        <developerConnection>scm:svn:https://svn.middleware.georgetown.edu/java-idp/</developerConnection>
+        <tag>HEAD</tag>
+        <url>http://svn.middleware.georgetown.edu/view/?root=java-idp</url>
+    </scm>
+
+    <developers>
+        <developer>
+            <id>cantor</id>
+            <name>Scott Cantor</name>
+            <organization>The Ohio State University</organization>
+            <organizationUrl>http://www.osu.edu/</organizationUrl>
+            <roles>
+                <role>developer</role>
+            </roles>
+            <timezone>-5</timezone>
+        </developer>
+        <developer>
+            <id>ndk</id>
+            <name>Nate Klingenstein</name>
+            <organization>Internet2</organization>
+            <organizationUrl>http://www.internet2.edu/</organizationUrl>
+            <roles>
+                <role>documentation</role>
+            </roles>
+            <timezone>-7</timezone>
+        </developer>
+        <developer>
+            <id>lajoie</id>
+            <name>Chad La Joie</name>
+            <organization>Itumi, LLC</organization>
+            <organizationUrl>http://www.itumi.biz/</organizationUrl>
+            <roles>
+                <role>developer</role>
+                <role>documentation</role>
+            </roles>
+            <timezone>-5</timezone>
+        </developer>
+        <developer>
+            <id>wnorris</id>
+            <name>Will Norris</name>
+            <organization>Google, Inc.</organization>
+            <organizationUrl>http://www.google.com/</organizationUrl>
+            <roles>
+                <role>developer</role>
+            </roles>
+            <timezone>-8</timezone>
+        </developer>
+        <developer>
+            <id>rdw</id>
+            <name>Rod Widdowson</name>
+            <organization>University of Edinburgh</organization>
+            <organizationUrl>http://www.ed.ac.uk/</organizationUrl>
+            <roles>
+                <role>developer</role>
+            </roles>
+            <timezone>0</timezone>
+        </developer>
+    </developers>
 </project>
index 31b9949..1cb0751 100644 (file)
         <ph:RequestPath>/SAML2/Redirect/SSO</ph:RequestPath>
     </ph:ProfileHandler>
 
+    <ph:ProfileHandler xsi:type="ph:SAML2SLO" 
+                    inboundBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+                    outboundBindingEnumeration="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect">
+        <ph:RequestPath>/SAML2/Redirect/SLO</ph:RequestPath>
+    </ph:ProfileHandler>
+
+    <ph:ProfileHandler xsi:type="ph:SAML2SLO" 
+                    inboundBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+                    outboundBindingEnumeration="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
+        <ph:RequestPath>/SAML2/POST/SLO</ph:RequestPath>
+    </ph:ProfileHandler>
+
+    <ph:ProfileHandler xsi:type="ph:SAML2SLO" 
+                    inboundBinding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+                    outboundBindingEnumeration="urn:oasis:names:tc:SAML:2.0:bindings:SOAP">
+        <ph:RequestPath>/SAML2/SOAP/SLO</ph:RequestPath>
+    </ph:ProfileHandler>
+    
     <ph:ProfileHandler xsi:type="ph:SAML2SSO" inboundBinding="urn:mace:shibboleth:2.0:profiles:AuthnRequest" 
                        outboundBindingEnumeration="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign
                                                    urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST
@@ -79,9 +97,9 @@
     </ph:ProfileHandler>
     
     <!-- Login Handlers -->
-    <ph:LoginHandler xsi:type="ph:RemoteUser">
+    <!-- <ph:LoginHandler xsi:type="ph:RemoteUser">
         <ph:AuthenticationMethod>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</ph:AuthenticationMethod>
-    </ph:LoginHandler>
+    </ph:LoginHandler>-->
     
     <!-- Login handler that delegates the act of authentication to an external system. -->
     <!-- This login handler and the RemoteUser login handler will be merged in the next major release. -->
     </ph:LoginHandler>
     -->
     
-    <!--  Username/password login handler -->
-    <!-- 
+    <!--  Username/password login handler -->   
     <ph:LoginHandler xsi:type="ph:UsernamePassword" 
                   jaasConfigurationLocation="file://$IDP_HOME$/conf/login.config">
         <ph:AuthenticationMethod>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</ph:AuthenticationMethod>
     </ph:LoginHandler>
-    -->
+    
     
     <!-- 
         Removal of this login handler will disable SSO support, that is it will require the user to authenticate 
index 2af48fa..2af01e6 100644 (file)
                                  encryptAssertions="conditional" encryptNameIds="never"/>
         
         <rp:ProfileConfiguration xsi:type="saml:SAML2ArtifactResolutionProfile" 
-                                 signResponses="never" signAssertions="always" 
-                                 encryptAssertions="conditional" encryptNameIds="never"/>
+                              signResponses="never"
+                              signAssertions="always"
+                              encryptAssertions="conditional"
+                              encryptNameIds="never"/>
+
+        <rp:ProfileConfiguration xsi:type="saml:SAML2LogoutRequestProfile"
+                              signResponses="always"
+                              signAssertions="never"
+                              encryptAssertions="never"
+                              encryptNameIds="never"
+                             frontChannelResponseTimeout="20000"
+                              backChannelConnectionPoolTimeout="2000"
+                              backChannelConnectionTimeout="2000"
+                              backChannelResponseTimeout="5000"  />
         
     </rp:DefaultRelyingParty>
         
diff --git a/src/installer/resources/conf-tmpl/tc-config.xml b/src/installer/resources/conf-tmpl/tc-config.xml
new file mode 100644 (file)
index 0000000..0a8c47b
--- /dev/null
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<tc:tc-config xmlns:tc="http://www.terracotta.org/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.terracotta.org/config http://www.terracotta.org/schema/terracotta-4.xsd">
+
+    <!--
+        Terracotta configuration file for Shibboleth.
+        
+        Complete documentation on the contents of this file may be found here:
+        http://terracotta.org/web/display/docs/Configuration+Guide+and+Reference
+    -->
+
+    <servers>
+        <!-- EXAMPLE SERVER CONFIGURATION -->
+        <!-- 
+            <server name="UNIQUE_ID" host="HOST">
+            <dso>
+            <persistence>
+            <mode>permanent-store</mode>
+            </persistence>
+            </dso>
+            
+            <logs>$IDP_HOME$/cluster/server/logs</logs>
+            <data>$IDP_HOME$/cluster/server/data</data>
+            <statistics>$IDP_HOME$/cluster/server/stats</statistics>
+            </server>
+        -->
+        <!-- START Terracotta server definitions -->
+
+
+        <!-- END Terracotta server definitions -->
+
+        <ha>
+            <mode>networked-active-passive</mode>
+            <networked-active-passive>
+                <election-time></election-time>
+            </networked-active-passive>
+        </ha>
+    </servers>
+
+    <system>
+        <configuration-model>production</configuration-model>
+    </system>
+
+    <clients>
+        <logs>$IDP_HOME$/cluster/client/logs-%i</logs>
+        <statistics>$IDP_HOME$/cluster/client/stats-%i</statistics>
+        <modules>
+            <module name="tim-vector" version="2.3.1" group-id="org.terracotta.modules"/>
+        </modules>
+    </clients>
+
+    <application>
+        <dso>
+            <additional-boot-jar-classes>
+                <include>javax.security.auth.Subject</include>
+                <include>javax.security.auth.Subject$SecureSet</include>
+                <include>javax.security.auth.x500.X500Principal</include>
+                <include>javax.security.auth.kerberos.KerberosPrincipal</include>
+            </additional-boot-jar-classes>
+            <roots>
+                <root>
+                    <root-name>storageService</root-name>
+                    <field-name>edu.internet2.middleware.shibboleth.common.util.EventingMapBasedStorageService.store</field-name>
+                </root>
+            </roots>
+            <instrumented-classes>
+                <include>
+                    <class-expression>org.opensaml.xml.util.LazyList</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.vt.middleware.ldap.jaas.LdapPrincipal</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.authn.UsernamePrincipal</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include> 
+                <include>
+                    <class-expression>edu.vt.middleware.ldap.jaas.LdapCredential</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.authn.AuthenticationException</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>org.opensaml.util.storage.AbstractExpiringObject</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.attributeDefinition.TransientIdEntry</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.authn.LoginContextEntry</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.authn.LoginContext</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.authn.ShibbolethSSOLoginContext</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>org.opensaml.util.storage.ReplayCacheEntry</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.session.impl.SessionManagerEntry</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.common.session.impl.AbstractSession</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.session.impl.SessionImpl</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>org.opensaml.common.binding.artifact.BasicSAMLArtifactMapEntry</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContextEntry</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext$LogoutStatus</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+                <include>
+                    <class-expression>edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext$LogoutInformation</class-expression>
+                    <honor-transient>true</honor-transient>
+                </include>
+            </instrumented-classes>
+            <locks>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.vt.middleware.ldap.jaas.LdapPrincipal.*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.authn.LoginContext.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.authn.LoginContext.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.authn.ShibbolethSSOLoginContext.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.authn.ShibbolethSSOLoginContext.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.common.session.impl.AbstractSession.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.common.session.impl.AbstractSession.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.session.impl.SessionImpl.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.session.impl.SessionImpl.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.session.impl.AuthenticationMethodInformationImpl.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext$LogoutInformation.get*(..)</method-expression>
+                    <lock-level>read</lock-level>
+                </autolock>
+                <autolock auto-synchronized="false">
+                    <method-expression>* edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext$LogoutInformation.set*(..)</method-expression>
+                    <lock-level>write</lock-level>
+                </autolock>
+            </locks>
+        </dso>
+    </application>
+
+</tc:tc-config>
\ No newline at end of file
index 09464be..9760aa4 100644 (file)
@@ -19,7 +19,20 @@ $IDP_CERTIFICATE$
         
         <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://$IDP_HOSTNAME$:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
 
-        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://$IDP_HOSTNAME$:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+                                   Location="https://$IDP_HOSTNAME$:8443/idp/profile/SAML2/SOAP/ArtifactResolution" 
+                                   index="2"/>
+        
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" 
+                             Location="https://$IDP_HOSTNAME$/idp/profile/SAML2/Redirect/SLO" 
+                             ResponseLocation="https://$IDP_HOSTNAME$/idp/profile/SAML2/Redirect/SLO"/>
+        
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" 
+                             Location="https://$IDP_HOSTNAME$/idp/profile/SAML2/POST/SLO" 
+                             ResponseLocation="https://$IDP_HOSTNAME$/idp/profile/SAML2/POST/SLO"/>
+        
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" 
+                             Location="https://$IDP_HOSTNAME$:8443/idp/profile/SAML2/SOAP/SLO" />
                                    
         <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
         <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
@@ -58,4 +71,4 @@ $IDP_CERTIFICATE$
         
     </AttributeAuthorityDescriptor>
     
-</EntityDescriptor>
+</EntityDescriptor>    
index fb5a9e2..ff99254 100644 (file)
@@ -32,6 +32,7 @@ import edu.internet2.middleware.shibboleth.idp.config.profile.saml1.SAML1Attribu
 import edu.internet2.middleware.shibboleth.idp.config.profile.saml1.ShibbolethSSOProfileHandlerBeanDefinitionParser;
 import edu.internet2.middleware.shibboleth.idp.config.profile.saml2.SAML2ArtifactResolutionProfileHandlerBeanDefinitionParser;
 import edu.internet2.middleware.shibboleth.idp.config.profile.saml2.SAML2AttributeQueryProfileHandlerBeanDefinitionParser;
+import edu.internet2.middleware.shibboleth.idp.config.profile.saml2.SAML2SLOProfileHandlerBeanDefinitionParser;
 import edu.internet2.middleware.shibboleth.idp.config.profile.saml2.SAML2ECPProfileHandlerBeanDefinitionParser;
 import edu.internet2.middleware.shibboleth.idp.config.profile.saml2.SAML2SSOProfileHandlerBeanDefinitionParser;
 
@@ -75,6 +76,8 @@ public class ProfileHandlerNamespaceHandler extends BaseSpringNamespaceHandler {
         registerBeanDefinitionParser(SAML2SSOProfileHandlerBeanDefinitionParser.SCHEMA_TYPE,
                 new SAML2SSOProfileHandlerBeanDefinitionParser());
 
+        registerBeanDefinitionParser(SAML2SLOProfileHandlerBeanDefinitionParser.SCHEMA_TYPE,
+                new SAML2SLOProfileHandlerBeanDefinitionParser());
         registerBeanDefinitionParser(SAML2ECPProfileHandlerBeanDefinitionParser.SCHEMA_TYPE,
                 new SAML2ECPProfileHandlerBeanDefinitionParser());
 
@@ -99,4 +102,4 @@ public class ProfileHandlerNamespaceHandler extends BaseSpringNamespaceHandler {
         registerBeanDefinitionParser(IPAddressLoginHandlerBeanDefinitionParser.SCHEMA_TYPE,
                 new IPAddressLoginHandlerBeanDefinitionParser());
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/config/profile/saml2/SAML2SLOProfileHandlerBeanDefinitionParser.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/config/profile/saml2/SAML2SLOProfileHandlerBeanDefinitionParser.java
new file mode 100644 (file)
index 0000000..217f810
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.config.profile.saml2;
+
+import edu.internet2.middleware.shibboleth.idp.config.profile.ProfileHandlerNamespaceHandler;
+import edu.internet2.middleware.shibboleth.idp.profile.saml2.SLOProfileHandler;
+import javax.xml.namespace.QName;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.w3c.dom.Element;
+
+/**
+ *
+ */
+public class SAML2SLOProfileHandlerBeanDefinitionParser extends AbstractSAML2ProfileHandlerBeanDefinitionParser {
+
+    /** Schema type. */
+    public static final QName SCHEMA_TYPE =
+            new QName(ProfileHandlerNamespaceHandler.NAMESPACE, "SAML2SLO");
+
+    /** {@inheritDoc} */
+    @Override
+    protected Class getBeanClass(Element arg0) {
+        return SLOProfileHandler.class;
+    }
+
+    @Override
+    protected void doParse(Element config, BeanDefinitionBuilder builder) {
+        super.doParse(config, builder);
+    }
+}
index e75821b..a2ed667 100644 (file)
@@ -58,10 +58,14 @@ import edu.internet2.middleware.shibboleth.common.relyingparty.ProfileConfigurat
 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml1.ShibbolethSSOConfiguration;
+import edu.internet2.middleware.shibboleth.common.session.SessionManager;
 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
 import edu.internet2.middleware.shibboleth.idp.authn.ShibbolethSSOLoginContext;
+import edu.internet2.middleware.shibboleth.idp.session.Session;
+import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
 import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
 import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
+import org.opensaml.saml1.core.NameIdentifier;
 
 /** Shibboleth SSO request profile handler. */
 public class ShibbolethSSOProfileHandler extends AbstractSAML1ProfileHandler {
@@ -271,6 +275,19 @@ public class ShibbolethSSOProfileHandler extends AbstractSAML1ProfileHandler {
             }
 
             samlResponse = buildResponse(requestContext, statements);
+
+            NameIdentifier nameID = buildNameId(requestContext);
+            Session session =
+                    getUserSession(requestContext.getInboundMessageTransport());
+            ServiceInformationImpl serviceInfo =
+                    (ServiceInformationImpl) session.getServicesInformation().get(requestContext.getPeerEntityId());
+            serviceInfo.setShibbolethNameIdentifier(nameID);
+            //index session by nameid
+            SessionManager<Session> sessionManager = getSessionManager();
+            String index = sessionManager.getIndexFromNameID(nameID);
+            if (index != null) {
+                sessionManager.indexSession(session, index);
+            }
         } catch (ProfileException e) {
             samlResponse = buildErrorResponse(requestContext);
         }
@@ -479,4 +496,4 @@ public class ShibbolethSSOProfileHandler extends AbstractSAML1ProfileHandler {
             spAssertionConsumerService = acs;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/SLOProfileHandler.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/profile/saml2/SLOProfileHandler.java
new file mode 100644 (file)
index 0000000..218ffd8
--- /dev/null
@@ -0,0 +1,1115 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.profile.saml2;
+
+import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
+import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
+import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
+import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.LogoutRequestConfiguration;
+import edu.internet2.middleware.shibboleth.common.session.SessionManager;
+import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
+import edu.internet2.middleware.shibboleth.idp.session.Session;
+import edu.internet2.middleware.shibboleth.idp.slo.HTTPClientInTransportAdapter;
+import edu.internet2.middleware.shibboleth.idp.slo.HTTPClientOutTransportAdapter;
+import edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext;
+import edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext.LogoutInformation;
+import edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContextStorageHelper;
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.httpclient.ConnectionPoolTimeoutException;
+import org.apache.commons.httpclient.HostConfiguration;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpConnection;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpState;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.URI;
+import org.apache.commons.httpclient.URIException;
+import org.apache.commons.httpclient.contrib.ssl.EasySSLProtocolSocketFactory;
+import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.params.HttpConnectionParams;
+import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
+import org.joda.time.DateTime;
+import org.opensaml.common.SAMLObjectBuilder;
+import org.opensaml.common.SAMLVersion;
+import org.opensaml.common.binding.BasicEndpointSelector;
+import org.opensaml.common.binding.BasicSAMLMessageContext;
+import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
+import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
+import org.opensaml.common.xml.SAMLConstants;
+import org.opensaml.saml2.binding.decoding.HTTPSOAP11Decoder;
+import org.opensaml.saml2.binding.encoding.HTTPSOAP11Encoder;
+import org.opensaml.saml2.core.Issuer;
+import org.opensaml.saml2.core.LogoutRequest;
+import org.opensaml.saml2.core.LogoutResponse;
+import org.opensaml.saml2.core.NameID;
+import org.opensaml.saml2.core.Status;
+import org.opensaml.saml2.core.StatusCode;
+import org.opensaml.saml2.core.impl.NameIDImpl;
+import org.opensaml.saml2.metadata.AttributeConsumingService;
+import org.opensaml.saml2.metadata.Endpoint;
+import org.opensaml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml2.metadata.Organization;
+import org.opensaml.saml2.metadata.OrganizationDisplayName;
+import org.opensaml.saml2.metadata.RoleDescriptor;
+import org.opensaml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml2.metadata.ServiceName;
+import org.opensaml.saml2.metadata.SingleLogoutService;
+import org.opensaml.saml2.metadata.provider.MetadataProvider;
+import org.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.opensaml.ws.message.decoder.MessageDecodingException;
+import org.opensaml.ws.message.encoder.MessageEncodingException;
+import org.opensaml.ws.soap.client.http.HttpClientBuilder;
+import org.opensaml.ws.transport.http.HTTPInTransport;
+import org.opensaml.ws.transport.http.HTTPOutTransport;
+import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
+import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
+import org.opensaml.xml.security.SecurityException;
+import org.opensaml.xml.security.credential.Credential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+public class SLOProfileHandler extends AbstractSAML2ProfileHandler {
+
+    private static final Logger log =
+            LoggerFactory.getLogger(SLOProfileHandler.class);
+    public static final String IDP_INITIATED_LOGOUT_ATTR =
+            "IDP_INITIATED_LOGOUT";
+    public static final String SKIP_LOGOUT_QUESTION_ATTR =
+            "SKIP_LOGOUT_QUESTION";
+    private final SAMLObjectBuilder<SingleLogoutService> sloServiceBuilder;
+    private final SAMLObjectBuilder<LogoutResponse> responseBuilder;
+    private final SAMLObjectBuilder<NameID> nameIDBuilder;
+    private final SAMLObjectBuilder<LogoutRequest> requestBuilder;
+    private final SAMLObjectBuilder<Issuer> issuerBuilder;
+
+    public SLOProfileHandler() {
+        super();
+        sloServiceBuilder = (SAMLObjectBuilder<SingleLogoutService>) getBuilderFactory().getBuilder(
+                SingleLogoutService.DEFAULT_ELEMENT_NAME);
+        responseBuilder =
+                (SAMLObjectBuilder<LogoutResponse>) getBuilderFactory().getBuilder(LogoutResponse.DEFAULT_ELEMENT_NAME);
+        nameIDBuilder =
+                (SAMLObjectBuilder<NameID>) getBuilderFactory().getBuilder(NameID.DEFAULT_ELEMENT_NAME);
+        requestBuilder =
+                (SAMLObjectBuilder<LogoutRequest>) getBuilderFactory().getBuilder(LogoutRequest.DEFAULT_ELEMENT_NAME);
+        issuerBuilder =
+                (SAMLObjectBuilder<Issuer>) getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+    }
+
+    @Override
+    protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+
+        if (requestContext.getInboundSAMLMessage() instanceof LogoutRequest) {
+            LogoutRequest request =
+                    (LogoutRequest) requestContext.getInboundSAMLMessage();
+
+            if (request != null) {
+                request.getSessionIndexes(); //TODO session indexes?
+
+                requestContext.setPeerEntityId(request.getIssuer().getValue());
+                requestContext.setInboundSAMLMessageId(request.getID());
+                if (request.getNameID() != null) {
+                    requestContext.setSubjectNameIdentifier(request.getNameID());
+                } else if (request.getEncryptedID() != null) {
+                    requestContext.setSubjectNameIdentifier(request.getEncryptedID());
+                } else {
+                    throw new ProfileException("Incoming Logout Request did not contain SAML2 NameID.");
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+        super.populateRelyingPartyInformation(requestContext);
+
+        EntityDescriptor relyingPartyMetadata =
+                requestContext.getPeerEntityMetadata();
+        if (relyingPartyMetadata != null) {
+            requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
+            requestContext.setPeerEntityRoleMetadata(relyingPartyMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS));
+        }
+    }
+
+    @Override
+    protected Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+        Endpoint endpoint = null;
+
+        if (getInboundBinding().equals(SAMLConstants.SAML2_SOAP11_BINDING_URI)) {
+
+            endpoint = sloServiceBuilder.buildObject();
+            endpoint.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI);
+        } else {
+            BasicEndpointSelector endpointSelector = new BasicEndpointSelector();
+            endpointSelector.setEndpointType(SingleLogoutService.DEFAULT_ELEMENT_NAME);
+            endpointSelector.setMetadataProvider(getMetadataProvider());
+            endpointSelector.setEntityMetadata(requestContext.getPeerEntityMetadata());
+            endpointSelector.setEntityRoleMetadata(requestContext.getPeerEntityRoleMetadata());
+            endpointSelector.setSamlRequest(requestContext.getInboundSAMLMessage());
+            endpointSelector.getSupportedIssuerBindings().addAll(getSupportedOutboundBindings());
+            endpoint = endpointSelector.selectEndpoint();
+        }
+
+        return endpoint;
+    }
+
+    @Override
+    public String getProfileId() {
+        return LogoutRequestConfiguration.PROFILE_ID;
+    }
+
+    public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport)
+            throws ProfileException {
+
+        HttpServletRequest servletRequest =
+                ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
+        SingleLogoutContext sloContext =
+                SingleLogoutContextStorageHelper.getSingleLogoutContext(servletRequest);
+
+        //TODO RelayState is lost?!
+        //TODO catch profileexception and respond with saml error.
+        if (servletRequest.getParameter("SAMLResponse") != null) {
+            log.debug("Processing incoming SAML LogoutResponse");
+            processLogoutResponse(sloContext, inTransport, outTransport);
+        } else if (servletRequest.getParameter("finish") != null) { //Front-channel case only
+            //TODO this is just a hack
+            if (sloContext.getRequesterEntityID() != null) {
+                InitialLogoutRequestContext initialRequest =
+                        buildRequestContext(sloContext, inTransport, outTransport);
+                respondToInitialRequest(sloContext, initialRequest);
+            }
+        } else if (servletRequest.getParameter("action") != null) { //Front-channel case only, called by SLOServlet?action
+            LogoutInformation nextActive = null;
+            //try to retrieve the sp from request parameter
+            String spEntityID = servletRequest.getParameter("entityID");
+            if (spEntityID != null) {
+                spEntityID = spEntityID.trim();
+                nextActive = sloContext.getServiceInformation().get(spEntityID);
+            }
+            if (nextActive == null) {
+                throw new ProfileException("Requested SP could not be found");
+            }
+            if (!nextActive.isLoggedIn()) {
+                throw new ProfileException("Already attempted to log out this service");
+            }
+
+            initiateFrontChannelLogout(sloContext, nextActive, outTransport);
+        } else {
+            processLogoutRequest(inTransport, outTransport);
+        }
+    }
+
+    /**
+     * Tries to decode logout response.
+     * 
+     * @param inTransport
+     * @param outTransport
+     * @throws ProfileException
+     */
+    protected boolean processLogoutResponse(SingleLogoutContext sloContext,
+            HTTPInTransport inTransport, HTTPOutTransport outTransport)
+            throws ProfileException {
+
+        LogoutRequestContext requestCtx = new LogoutRequestContext();
+        requestCtx.setInboundMessageTransport(inTransport);
+        SAMLMessageDecoder decoder =
+                getMessageDecoders().get(getInboundBinding());
+        LogoutResponse logoutResponse;
+        try {
+            decoder.decode(requestCtx);
+            logoutResponse = requestCtx.getInboundSAMLMessage();
+        } catch (MessageDecodingException ex) {
+            log.warn("Cannot decode LogoutResponse", ex);
+            throw new ProfileException(ex);
+        } catch (SecurityException ex) {
+            log.warn("Exception while validating LogoutResponse", ex);
+            throw new ProfileException(ex);
+        } catch (ClassCastException ex) {
+            log.debug("Cannot decode LogoutResponse", ex);
+            //this is the case when inbound message is LogoutRequest, so return silently
+            return false;
+        }
+
+        String inResponseTo = logoutResponse.getInResponseTo();
+        String spEntityID = requestCtx.getInboundMessageIssuer();
+
+        log.debug("Received response from '{}' to request '{}'", spEntityID, inResponseTo);
+        LogoutInformation serviceLogoutInfo =
+                sloContext.getServiceInformation().get(spEntityID);
+        if (serviceLogoutInfo == null) {
+            throw new ProfileException("LogoutResponse issuer is unknown");
+        }
+        if (!serviceLogoutInfo.getLogoutRequestId().equals(inResponseTo)) {
+            serviceLogoutInfo.setLogoutFailed();
+            throw new ProfileException("LogoutResponse InResponseTo does not match the LogoutRequest ID");
+        }
+        log.info("Logout status is '{}'", logoutResponse.getStatus().getStatusCode().getValue().toString());
+        if (logoutResponse.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) {
+            serviceLogoutInfo.setLogoutSucceeded();
+        } else {
+            serviceLogoutInfo.setLogoutFailed();
+        }
+
+        return true;
+    }
+
+    /**
+     * Continue logout processing.
+     * 
+     * @param inTransport
+     * @param outTransport
+     * @throws ProfileException
+     */
+    protected void processLogoutRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport)
+            throws ProfileException {
+
+        HttpServletRequest servletRequest =
+                ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
+        Session idpSession = getUserSession(inTransport);
+        boolean idpInitiatedLogout =
+                servletRequest.getAttribute(IDP_INITIATED_LOGOUT_ATTR) != null;
+        InitialLogoutRequestContext initialRequest = null;
+
+        if (idpInitiatedLogout) {
+            //idp initiated logout
+            log.info("Starting the IdP-initiated logout process");
+            initialRequest = createInitialLogoutRequestContext();
+            initialRequest.setInboundMessageTransport(inTransport);
+            servletRequest.setAttribute(SKIP_LOGOUT_QUESTION_ATTR, true);
+        } else {
+            //sp initiated logout
+            initialRequest = new InitialLogoutRequestContext();
+            log.info("Processing incoming LogoutRequest");
+            decodeRequest(initialRequest, inTransport, outTransport);
+            checkSamlVersion(initialRequest);
+
+            //if session is null, try to find nameid-bound one
+            if (idpSession == null) {
+                NameID nameID =
+                        initialRequest.getInboundSAMLMessage().getNameID();
+                SessionManager<Session> sessionManager = getSessionManager();
+                String nameIDIndex = sessionManager.getIndexFromNameID(nameID);
+                log.info("Session not found in request, trying to resolve session from NameID '{}'",
+                        nameIDIndex);
+                idpSession = sessionManager.getSession(nameIDIndex);
+            }
+        }
+        if (idpSession == null) {
+            log.warn("Cannot find IdP Session");
+            initialRequest.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.UNKNOWN_PRINCIPAL_URI, null));
+            throw new ProfileException("Cannot find IdP Session for principal");
+        }
+
+        if (!idpInitiatedLogout
+                && !idpSession.getServicesInformation().keySet().
+                contains(initialRequest.getInboundMessageIssuer())) {
+            String msg = "Requesting entity is not session participant";
+            log.warn(msg);
+            initialRequest.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, StatusCode.REQUEST_DENIED_URI, msg));
+            throw new ProfileException(msg);
+        }
+
+        SingleLogoutContext sloContext =
+                buildSingleLogoutContext(initialRequest, idpSession);
+        destroySession(sloContext);
+
+        if (getInboundBinding().equals(SAMLConstants.SAML2_SOAP11_BINDING_URI)) {
+            log.info("Issuing Backchannel logout requests");
+            initiateBackChannelLogout(sloContext);
+
+            respondToInitialRequest(sloContext, initialRequest);
+        } else {
+            //skip logout question if the requesting sp is the only session participant
+            if (!idpInitiatedLogout && sloContext.getServiceInformation().size() == 1) {
+                servletRequest.setAttribute(SKIP_LOGOUT_QUESTION_ATTR, true);
+            }
+            HttpServletResponse servletResponse =
+                    ((HttpServletResponseAdapter) outTransport).getWrappedResponse();
+            SingleLogoutContextStorageHelper.bindSingleLogoutContext(sloContext, servletRequest);
+            populateServiceDisplayNames(sloContext);
+            try {
+                servletRequest.getRequestDispatcher("/SLOServlet").forward(servletRequest, servletResponse);
+            } catch (ServletException ex) {
+                String msg = "Cannot forward request to SLO Servlet";
+                log.error(msg, ex);
+                initialRequest.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, msg));
+                throw new ProfileException(ex);
+            } catch (IOException ex) {
+                String msg = "Cannot forward request to SLO Servlet";
+                log.error(msg, ex);
+                initialRequest.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, msg));
+                throw new ProfileException(ex);
+            }
+        }
+    }
+
+    /**
+     * Issue back-channel logout requests to all session participants.
+     * 
+     * @param idpSession
+     * @return
+     * @throws ProfileException
+     */
+    public SingleLogoutContext administrativeLogout(Session idpSession) throws ProfileException {
+        log.info("Administratively logging out user '{}'", idpSession.getPrincipalName());
+        InitialLogoutRequestContext initialRequest = createInitialLogoutRequestContext();
+        SingleLogoutContext sloContext = SingleLogoutContext.createInstance(null, initialRequest, idpSession);
+        try {
+            initiateBackChannelLogout(sloContext);
+        } catch (ProfileException e) {
+            log.error("Exception was caught while administratively logging out user '{}'",
+                    idpSession.getPrincipalName(), e);
+        }
+        destroySession(sloContext);
+        return sloContext;
+    }
+
+    /**
+     * Creates SAML2 LogoutRequest and corresponding context.
+     *
+     * @param sloContext
+     * @param serviceLogoutInfo
+     * @param endpoint
+     * @return
+     */
+    private LogoutRequestContext createLogoutRequestContext(
+            SingleLogoutContext sloContext,
+            LogoutInformation serviceLogoutInfo,
+            Endpoint endpoint) {
+
+        String spEntityID = serviceLogoutInfo.getEntityID();
+        log.debug("Trying SP: {}", spEntityID);
+        LogoutRequest request = buildLogoutRequest(sloContext);
+
+        serviceLogoutInfo.setLogoutRequestId(request.getID());
+
+        NameID nameId = buildNameID(serviceLogoutInfo);
+        if (nameId == null) {
+            log.info("NameID is null, cannot crete logout request context");
+            return null;
+        }
+
+        request.setNameID(nameId);
+        request.setDestination(endpoint.getLocation());
+
+        LogoutRequestContext requestCtx = new LogoutRequestContext();
+        requestCtx.setCommunicationProfileId(getProfileId());
+        requestCtx.setSecurityPolicyResolver(getSecurityPolicyResolver());
+        requestCtx.setOutboundMessageIssuer(sloContext.getResponderEntityID());
+        requestCtx.setInboundMessageIssuer(spEntityID);
+        requestCtx.setPeerEntityEndpoint(endpoint);
+        requestCtx.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
+        //TODO get credential configured for relying party
+        Credential signingCredential =
+                getRelyingPartyConfigurationManager().
+                getDefaultRelyingPartyConfiguration().getDefaultSigningCredential();
+        requestCtx.setOutboundSAMLMessageSigningCredential(signingCredential);
+        requestCtx.setOutboundSAMLMessage(request);
+
+        return requestCtx;
+    }
+
+    /**
+     * Destroy idp session.
+     *
+     * @param sloContext
+     */
+    private void destroySession(SingleLogoutContext sloContext) {
+        log.info("Invalidating session '{}'.", sloContext.getIdpSessionID());
+        getSessionManager().destroySession(sloContext.getIdpSessionID());
+    }
+
+    /**
+     * Issues back channel logout request to every session participant.
+     *
+     * @param sloContext
+     * @throws ProfileException
+     */
+    private void initiateBackChannelLogout(SingleLogoutContext sloContext) throws ProfileException {
+        for (LogoutInformation serviceLogoutInfo : sloContext.getServiceInformation().values()) {
+            if (serviceLogoutInfo.isLoggedIn()) {
+                try {
+                    initiateBackChannelLogout(sloContext, serviceLogoutInfo);
+                } catch (ProfileException ex) {
+                    log.warn("Caught exception while trying to issue LogoutRequest to '{}'",
+                            serviceLogoutInfo.getEntityID(), ex);
+                    serviceLogoutInfo.setLogoutFailed();
+                }
+            }
+        }
+    }
+
+    /**
+     * Issues back channel logout request to session participant.
+     *
+     * @param sloContext
+     * @param serviceLogoutInfo
+     * @throws ProfileException
+     */
+    private void initiateBackChannelLogout(SingleLogoutContext sloContext, LogoutInformation serviceLogoutInfo)
+            throws ProfileException {
+
+        if (!serviceLogoutInfo.isLoggedIn()) {
+            log.info("Logout status for entity is '{}', not attempting logout", serviceLogoutInfo.getLogoutStatus().toString());
+            return;
+        }
+
+        String spEntityID = serviceLogoutInfo.getEntityID();
+        Endpoint endpoint =
+                getEndpointForBinding(spEntityID, SAMLConstants.SAML2_SOAP11_BINDING_URI);
+        if (endpoint == null) {
+            log.info("No SAML2 LogoutRequest SOAP endpoint found for entity '{}'", spEntityID);
+            serviceLogoutInfo.setLogoutUnsupported();
+            return;
+        }
+
+        serviceLogoutInfo.setLogoutAttempted();
+        LogoutRequestContext requestCtx =
+                createLogoutRequestContext(sloContext, serviceLogoutInfo, endpoint);
+        if (requestCtx == null) {
+            log.info("Cannot create LogoutRequest Context for entity '{}'", spEntityID);
+            serviceLogoutInfo.setLogoutFailed();
+            return;
+        }
+        HttpConnection httpConn = null;
+        try {
+            //prepare http message exchange for soap
+            log.debug("Preparing HTTP transport for SOAP request");
+            httpConn = createHttpConnection(serviceLogoutInfo, endpoint);
+            if (httpConn == null) {
+                log.warn("Unable to acquire usable http connection from the pool");
+                serviceLogoutInfo.setLogoutFailed();
+                return;
+            }
+            log.debug("Opening HTTP connection to '{}'", endpoint.getLocation());
+            httpConn.open();
+            if (!httpConn.isOpen()) {
+                log.warn("HTTP connection could not be opened");
+                serviceLogoutInfo.setLogoutFailed();
+                return;
+            }
+
+            log.debug("Preparing transports and encoders/decoders");
+            prepareSOAPTransport(requestCtx, httpConn, endpoint);
+            SAMLMessageEncoder encoder = new HTTPSOAP11Encoder();
+            SAMLMessageDecoder decoder =
+                    new HTTPSOAP11Decoder(getParserPool());
+
+            //encode and sign saml request
+            encoder.encode(requestCtx);
+            //TODO: audit log is still missing
+
+            log.info("Issuing back-channel logout request to SP '{}'", spEntityID);
+            //execute SOAP/HTTP call
+            log.debug("Executing HTTP POST");
+            if (!requestCtx.execute(httpConn)) {
+                log.warn("Logout execution failed on SP '{}', HTTP status is '{}'",
+                        spEntityID, requestCtx.getHttpStatus());
+                serviceLogoutInfo.setLogoutFailed();
+
+                return;
+            }
+
+            //decode saml response
+            decoder.decode(requestCtx);
+
+            LogoutResponse spResponse = requestCtx.getInboundSAMLMessage();
+            StatusCode statusCode = spResponse.getStatus().getStatusCode();
+            if (statusCode.getValue().equals(StatusCode.SUCCESS_URI)) {
+                log.info("Logout was successful on SP '{}'.", spEntityID);
+                serviceLogoutInfo.setLogoutSucceeded();
+            } else {
+                log.warn("Logout failed on SP '{}', logout status code is '{}'.", spEntityID, statusCode.getValue());
+                StatusCode secondaryCode = statusCode.getStatusCode();
+                if (secondaryCode != null) {
+                    log.warn("Additional status code: '{}'", secondaryCode.getValue());
+                }
+                serviceLogoutInfo.setLogoutFailed();
+            }
+        } catch (SocketTimeoutException e) { //socket connect or read timeout
+            log.info("Socket timeout while sending SOAP request to SP '{}'",
+                    serviceLogoutInfo.getEntityID());
+            serviceLogoutInfo.setLogoutFailed();
+        } catch (IOException e) { //other networking error
+            log.info("IOException caught while sending SOAP request", e);
+            serviceLogoutInfo.setLogoutFailed();
+        } catch (Throwable t) { //unexpected
+            log.error("Unexpected exception caught while sending SAML Logout request", t);
+            serviceLogoutInfo.setLogoutFailed();
+        } finally { //
+            requestCtx.releaseConnection();
+            if (httpConn != null && httpConn.isOpen()) {
+                log.debug("Closing HTTP connection");
+                try {
+                    httpConn.close();
+                } catch (Throwable t) {
+                    log.warn("Caught exception while closing HTTP Connection", t);
+                }
+            }
+        }
+    }
+
+    private InitialLogoutRequestContext createInitialLogoutRequestContext() {
+        InitialLogoutRequestContext initialRequest = new InitialLogoutRequestContext();
+        RelyingPartyConfiguration defaultRPC =
+                getRelyingPartyConfigurationManager().getDefaultRelyingPartyConfiguration();
+        initialRequest.setLocalEntityId(defaultRPC.getProviderId());
+        initialRequest.setProfileConfiguration(
+                (LogoutRequestConfiguration) defaultRPC.getProfileConfiguration(getProfileId()));
+
+        return initialRequest;
+    }
+
+    /**
+     * Reads SAML2 SingleLogoutService endpoint of the entity or
+     * null if no metadata or endpoint found.
+     *
+     * @param spEntityID
+     * @param bindingURI which binding to use
+     * @return
+     */
+    private Endpoint getEndpointForBinding(String spEntityID, String bindingURI) {
+        RoleDescriptor spMetadata = null;
+        try {
+            //retrieve metadata
+            spMetadata =
+                    getMetadataProvider().getRole(spEntityID, SPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS);
+            if (spMetadata == null) {
+                log.warn("SP Metadata is null");
+                return null;
+            }
+        } catch (MetadataProviderException ex) {
+            log.info("Cannot get SAML2 metadata for SP '{}'.", spEntityID);
+            return null;
+        }
+
+        //find endpoint for SingleLogoutService
+        BasicEndpointSelector es = new BasicEndpointSelector();
+        es.setEndpointType(SingleLogoutService.DEFAULT_ELEMENT_NAME);
+        es.setMetadataProvider(getMetadataProvider());
+        es.getSupportedIssuerBindings().add(bindingURI);
+        es.setEntityRoleMetadata(spMetadata);
+        Endpoint endpoint = es.selectEndpoint();
+        if (endpoint == null) {
+            log.info("Cannot get SAML2 SingleLogoutService endpoint for SP '{}' and binding '{}'.", spEntityID, bindingURI);
+            return null;
+        }
+
+        return endpoint;
+    }
+
+    /**
+     * Builds NameID for the principal and the SP.
+     *
+     * TODO support encrypted nameid?
+     *
+     * @param serviceLogoutInfo
+     * @return
+     */
+    private NameID buildNameID(LogoutInformation serviceLogoutInfo) {
+        if (serviceLogoutInfo.getNameIdentifier() == null) {
+            return null;
+        }
+        NameID nameId = nameIDBuilder.buildObject();
+        nameId.setFormat(serviceLogoutInfo.getNameIdentifierFormat());
+        nameId.setValue(serviceLogoutInfo.getNameIdentifier());
+        nameId.setNameQualifier(serviceLogoutInfo.getNameQualifier());
+        nameId.setSPNameQualifier(serviceLogoutInfo.getSPNameQualifier());
+
+        return nameId;
+    }
+
+    /**
+     * Build SAML request for issuing LogoutRequest.
+     * 
+     * @param sloContext
+     * @param spEntityID
+     * @return
+     */
+    private LogoutRequest buildLogoutRequest(SingleLogoutContext sloContext) {
+        LogoutRequest request = requestBuilder.buildObject();
+        //build saml request
+        DateTime issueInstant = new DateTime();
+        request.setIssueInstant(issueInstant);
+        request.setID(getIdGenerator().generateIdentifier());
+        request.setVersion(SAMLVersion.VERSION_20);
+        Issuer issuer = issuerBuilder.buildObject();
+        issuer.setValue(sloContext.getResponderEntityID());
+        request.setIssuer(issuer);
+
+        return request;
+    }
+
+    /**
+     * Populate service display names from metadata.
+     * This method must be called once.
+     *
+     * @param sloContext
+     */
+    private void populateServiceDisplayNames(SingleLogoutContext sloContext) {
+        MetadataProvider mdProvider = getMetadataProvider();
+        for (LogoutInformation serviceInfo : sloContext.getServiceInformation().values()) {
+            EntityDescriptor spMetadata;
+            String spEntityID = serviceInfo.getEntityID();
+            try {
+                spMetadata = mdProvider.getEntityDescriptor(spEntityID);
+            } catch (MetadataProviderException ex) {
+                log.warn("Can not get metadata for relying party '{}'", spEntityID);
+                continue;
+            }
+            Map<String, String> serviceNames = extractServiceNames(spMetadata);
+            if (serviceNames != null && serviceNames.size() > 0) {
+                serviceInfo.setDisplayName(serviceNames);
+            } else {
+                Map<String, String> organizationDNames = extractOrganizationDisplayNames(spMetadata);
+                if (organizationDNames != null && organizationDNames.size() > 0) {
+                    serviceInfo.setDisplayName(organizationDNames);
+                }
+            }
+        }
+    }
+
+    /**
+     * Extracts ServiceName information from SP Entity Descriptor.
+     * 
+     * @param spMetadata
+     * @return
+     */
+    private Map<String, String> extractServiceNames(EntityDescriptor spMetadata) {
+        String spEntityID = spMetadata.getEntityID();
+        SPSSODescriptor spDescr = spMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS);
+        if (spDescr == null) {
+            log.debug("No SAML SPSSODescriptor found for relying party '{}'", spEntityID);
+            return null;
+        }
+        AttributeConsumingService attrCs = spDescr.getDefaultAttributeConsumingService();
+        if (attrCs == null) {
+            List<AttributeConsumingService> attrCSList = spDescr.getAttributeConsumingServices();
+            if (attrCSList != null && !attrCSList.isEmpty()) {
+                attrCs = attrCSList.get(0);
+            }
+        }
+        if (attrCs == null) {
+            log.debug("No AttributeConsumingService found for relying party '{}'", spEntityID);
+            return null;
+        }
+        List<ServiceName> sNameList = attrCs.getNames();
+        if (sNameList == null) {
+            log.debug("No ServiceName found for relying party '{}'", spEntityID);
+            return null;
+        }
+        Map<String, String> serviceNames =
+                new HashMap<String, String>(sNameList.size());
+        for (ServiceName sName : sNameList) {
+            serviceNames.put(sName.getName().getLanguage(), sName.getName().getLocalString());
+        }
+        return serviceNames;
+    }
+
+    /**
+     * Extracts OrganizationDisplayName information from SP Entity Descriptor.
+     *
+     * @param spMetadata
+     * @return
+     */
+    private Map<String, String> extractOrganizationDisplayNames(EntityDescriptor spMetadata) {
+        String spEntityID = spMetadata.getEntityID();
+        Organization spOrg = spMetadata.getOrganization();
+        if (spOrg == null) {
+            log.debug("Organization is not set for relying party '{}'", spEntityID);
+            return null;
+        }
+        List<OrganizationDisplayName> dNameList =
+                spOrg.getDisplayNames();
+        if (dNameList == null) {
+            log.debug("DisplayName is unset for relying party '{}'", spEntityID);
+            return null;
+        }
+        Map<String, String> oDNames = new HashMap<String, String>(dNameList.size());
+        for (OrganizationDisplayName dName : dNameList) {
+            oDNames.put(dName.getName().getLanguage(), dName.getName().getLocalString());
+        }
+        return oDNames;
+    }
+
+    /**
+     * Creates Http connection.
+     *
+     * @param serviceLogoutInfo
+     * @param endpoint
+     * @return
+     * @throws URIException
+     * @throws GeneralSecurityException
+     * @throws IOException
+     */
+    private HttpConnection createHttpConnection(
+            LogoutInformation serviceLogoutInfo, Endpoint endpoint)
+            throws URIException, GeneralSecurityException, IOException {
+
+        HttpClientBuilder httpClientBuilder =
+                new HttpClientBuilder();
+        httpClientBuilder.setContentCharSet("UTF-8");
+        SecureProtocolSocketFactory sf = new EasySSLProtocolSocketFactory();
+        httpClientBuilder.setHttpsProtocolSocketFactory(sf);
+
+        //build http connection
+        HttpClient httpClient = httpClientBuilder.buildClient();
+
+        HostConfiguration hostConfig = new HostConfiguration();
+        URI location = new URI(endpoint.getLocation());
+        hostConfig.setHost(location);
+
+        LogoutRequestConfiguration config = (LogoutRequestConfiguration) getProfileConfiguration(
+                serviceLogoutInfo.getEntityID(), getProfileId());
+        if (log.isDebugEnabled()) {
+            log.debug("Creating new HTTP connection with the following timeouts:");
+            log.debug("Maximum waiting time for the connection pool is {}",
+                    config.getBackChannelConnectionPoolTimeout());
+            log.debug("Timeout for connection establishment is {}",
+                    config.getBackChannelConnectionTimeout());
+            log.debug("Timeout for soap response is {}",
+                    config.getBackChannelResponseTimeout());
+        }
+        HttpConnection httpConn = null;
+        try {
+            httpConn = httpClient.getHttpConnectionManager().
+                    getConnectionWithTimeout(hostConfig, config.getBackChannelConnectionPoolTimeout());
+        } catch (ConnectionPoolTimeoutException e) {
+            return null;
+        }
+
+        HttpConnectionParams params = new HttpConnectionParams();
+        params.setConnectionTimeout(config.getBackChannelConnectionTimeout());
+        params.setSoTimeout(config.getBackChannelResponseTimeout());
+        httpConn.setParams(params);
+
+        return httpConn;
+    }
+
+    /**
+     * Adapts SOAP/HTTP client transport to SAML transports.
+     * @param requestCtx
+     * @param httpConn
+     * @param endpoint
+     */
+    private void prepareSOAPTransport(LogoutRequestContext requestCtx,
+            HttpConnection httpConn, Endpoint endpoint) {
+
+        EntityEnclosingMethod method =
+                new PostMethod(endpoint.getLocation());
+        requestCtx.setPostMethod(method);
+        HTTPOutTransport soapOutTransport =
+                new HTTPClientOutTransportAdapter(httpConn, method);
+        HTTPInTransport soapInTransport =
+                new HTTPClientInTransportAdapter(httpConn, method);
+        requestCtx.setOutboundMessageTransport(soapOutTransport);
+        requestCtx.setInboundMessageTransport(soapInTransport);
+    }
+
+    /**
+     * Issues front and back channel logout requests to session participants.
+     * 
+     * @param inTransport
+     * @param outTransport
+     * @param initialRequest
+     * @param idpSession
+     * @throws ProfileException
+     */
+    private void initiateFrontChannelLogout(
+            SingleLogoutContext sloContext,
+            LogoutInformation serviceLogoutInfo,
+            HTTPOutTransport outTransport)
+            throws ProfileException {
+
+        if (!serviceLogoutInfo.isLoggedIn()) {
+            log.info("Logout status for entity is '{}', not attempting logout", serviceLogoutInfo.getLogoutStatus().toString());
+            return;
+        }
+
+        String spEntityID = serviceLogoutInfo.getEntityID();
+        //prefer HTTP-Redirect binding
+        Endpoint endpoint =
+                getEndpointForBinding(spEntityID, SAMLConstants.SAML2_REDIRECT_BINDING_URI);
+        if (endpoint == null) {
+            //fallback to HTTP-POST when no HTTP-Redirect is set
+            endpoint =
+                    getEndpointForBinding(spEntityID, SAMLConstants.SAML2_POST_BINDING_URI);
+        }
+        if (endpoint == null) {
+            log.info("No SAML2 LogoutRequest front-channel endpoint found for entity '{}'", spEntityID);
+            endpoint =
+                    getEndpointForBinding(spEntityID, SAMLConstants.SAML2_SOAP11_BINDING_URI);
+            if (endpoint != null) {
+                //fallback to SOAP1.1 when no HTTP-POST is set
+                initiateBackChannelLogout(sloContext, serviceLogoutInfo);
+            } else {
+                //no supported endpoints found
+                serviceLogoutInfo.setLogoutUnsupported();
+            }
+            return;
+        }
+        SAMLMessageEncoder encoder =
+                getMessageEncoders().get(endpoint.getBinding());
+        if (encoder == null) {
+            log.warn("No message encoder found for binding '{}'", endpoint.getBinding());
+            serviceLogoutInfo.setLogoutUnsupported();
+            return;
+        }
+
+        serviceLogoutInfo.setLogoutAttempted();
+        LogoutRequestContext requestCtx =
+                createLogoutRequestContext(sloContext, serviceLogoutInfo, endpoint);
+        if (requestCtx == null) {
+            log.info("Cannot create LogoutRequest Context for entity '{}'", spEntityID);
+            serviceLogoutInfo.setLogoutFailed();
+            return;
+        }
+        requestCtx.setOutboundMessageTransport(outTransport);
+
+        try {
+            encoder.encode(requestCtx);
+        } catch (MessageEncodingException ex) {
+            log.warn("Cannot encode LogoutRequest", ex);
+            serviceLogoutInfo.setLogoutFailed();
+            return;
+        }
+    }
+
+    /**
+     * Respond to LogoutRequest.
+     * 
+     * @param sloContext
+     * @param initialRequest
+     * @throws ProfileException
+     */
+    protected void respondToInitialRequest(SingleLogoutContext sloContext, InitialLogoutRequestContext initialRequest)
+            throws ProfileException {
+
+        boolean success = true;
+        for (SingleLogoutContext.LogoutInformation info : sloContext.getServiceInformation().values()) {
+            if (!info.getLogoutStatus().equals(SingleLogoutContext.LogoutStatus.LOGOUT_SUCCEEDED)) {
+                success = false;
+            }
+        }
+        Status status;
+        if (success) {
+            log.info("Status of Single Log-out: success");
+            status = buildStatus(StatusCode.SUCCESS_URI, null, null);
+        } else {
+            log.info("Status of Single Log-out: partial");
+            status =
+                    buildStatus(StatusCode.SUCCESS_URI, StatusCode.PARTIAL_LOGOUT_URI, null);
+        }
+
+        LogoutResponse samlResponse =
+                buildLogoutResponse(initialRequest, status);
+        populateRelyingPartyInformation(initialRequest);
+        Endpoint endpoint = selectEndpoint(initialRequest);
+        initialRequest.setPeerEntityEndpoint(endpoint);
+        initialRequest.setOutboundSAMLMessage(samlResponse);
+        initialRequest.setOutboundSAMLMessageId(samlResponse.getID());
+        initialRequest.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
+        Credential signingCredential =
+                initialRequest.getProfileConfiguration().getSigningCredential();
+        if (signingCredential == null) {
+            initialRequest.getRelyingPartyConfiguration().getDefaultSigningCredential();
+        }
+        initialRequest.setOutboundSAMLMessageSigningCredential(signingCredential);
+
+        log.debug("Sending response to the original LogoutRequest");
+        encodeResponse(initialRequest);
+        writeAuditLogEntry(initialRequest);
+    }
+
+    /**
+     * Builds new single log-out context for session store between logout events.
+     *
+     * @param initialRequest
+     * @param idpSession
+     * @return
+     */
+    private SingleLogoutContext buildSingleLogoutContext(InitialLogoutRequestContext initialRequest, Session idpSession) {
+        HttpServletRequest servletRequest =
+                ((HttpServletRequestAdapter) initialRequest.getInboundMessageTransport()).getWrappedRequest();
+
+        return SingleLogoutContext.createInstance(
+                HttpHelper.getRequestUriWithoutContext(servletRequest),
+                initialRequest,
+                idpSession);
+    }
+
+    /**
+     * Builds request context from information available after logout events.
+     *
+     * @param sloContext
+     * @return
+     */
+    protected InitialLogoutRequestContext buildRequestContext(SingleLogoutContext sloContext,
+            HTTPInTransport in, HTTPOutTransport out) throws ProfileException {
+
+        InitialLogoutRequestContext initialRequest = new InitialLogoutRequestContext();
+
+        initialRequest.setCommunicationProfileId(getProfileId());
+        initialRequest.setMessageDecoder(getMessageDecoders().get(getInboundBinding()));
+        initialRequest.setInboundMessageTransport(in);
+        initialRequest.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
+        initialRequest.setOutboundMessageTransport(out);
+        initialRequest.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
+        initialRequest.setMetadataProvider(getMetadataProvider());
+        initialRequest.setInboundSAMLMessageId(sloContext.getRequestSAMLMessageID());
+        initialRequest.setInboundMessageIssuer(sloContext.getRequesterEntityID());
+        initialRequest.setLocalEntityId(sloContext.getResponderEntityID());
+        initialRequest.setPeerEntityId(sloContext.getRequesterEntityID());
+        initialRequest.setSecurityPolicyResolver(getSecurityPolicyResolver());
+        initialRequest.setProfileConfiguration(
+                (LogoutRequestConfiguration) getProfileConfiguration(
+                sloContext.getRequesterEntityID(), getProfileId()));
+        initialRequest.setRelyingPartyConfiguration(
+                getRelyingPartyConfiguration(sloContext.getRequesterEntityID()));
+
+        return initialRequest;
+    }
+
+    /**
+     * Builds Logout Response.
+     *
+     * @param initialRequest
+     * @return
+     * @throws edu.internet2.middleware.shibboleth.common.profile.ProfileException
+     */
+    protected LogoutResponse buildLogoutResponse(
+            BaseSAML2ProfileRequestContext<?, ?, ?> initialRequest,
+            Status status)
+            throws ProfileException {
+
+        DateTime issueInstant = new DateTime();
+
+        LogoutResponse logoutResponse = responseBuilder.buildObject();
+        logoutResponse.setIssueInstant(issueInstant);
+        populateStatusResponse(initialRequest, logoutResponse);
+        logoutResponse.setStatus(status);
+
+        return logoutResponse;
+    }
+
+    /**
+     * Decodes an incoming request and populates a created request context with the resultant information.
+     *
+     * @param inTransport inbound message transport
+     * @param outTransport outbound message transport *
+     * @param initialRequest request context to which decoded information should be added
+     *
+     * @throws ProfileException throw if there is a problem decoding the request
+     */
+    protected void decodeRequest(InitialLogoutRequestContext initialRequest,
+            HTTPInTransport inTransport, HTTPOutTransport outTransport)
+            throws ProfileException {
+        log.debug("Decoding message with decoder binding '{}'", getInboundBinding());
+
+        initialRequest.setCommunicationProfileId(getProfileId());
+
+        MetadataProvider metadataProvider = getMetadataProvider();
+        initialRequest.setMetadataProvider(metadataProvider);
+
+        initialRequest.setInboundMessageTransport(inTransport);
+        initialRequest.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
+        initialRequest.setSecurityPolicyResolver(getSecurityPolicyResolver());
+        initialRequest.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
+
+        initialRequest.setOutboundMessageTransport(outTransport);
+        initialRequest.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
+
+        try {
+            SAMLMessageDecoder decoder =
+                    getInboundMessageDecoder(null);
+            initialRequest.setMessageDecoder(decoder);
+            decoder.decode(initialRequest);
+            log.debug("Decoded request from relying party '{}'", initialRequest.getInboundMessage());
+
+            //TODO
+            /*if (!(initialRequest.getInboundSAMLMessage() instanceof LogoutRequest)) {
+            log.warn("Incoming message was not a LogoutRequest, it was a {}", initialRequest.getInboundSAMLMessage().getClass().getName());
+            initialRequest.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, null,
+            "Invalid SAML LogoutRequest message."));
+            throw new ProfileException("Invalid SAML LogoutRequest message.");
+            }*/
+
+        } catch (MessageDecodingException e) {
+            String msg = "Error decoding logout request message";
+            log.warn(msg, e);
+            initialRequest.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, msg));
+            throw new ProfileException(msg);
+        } catch (SecurityException e) {
+            String msg = "Message did not meet security requirements";
+            log.warn(msg, e);
+            initialRequest.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI, msg));
+            throw new ProfileException(msg, e);
+        } finally {
+            // Set as much information as can be retrieved from the decoded message
+            populateRequestContext(initialRequest);
+        }
+    }
+
+    public class InitialLogoutRequestContext
+            extends BaseSAML2ProfileRequestContext<LogoutRequest, LogoutResponse, LogoutRequestConfiguration> {
+    }
+
+    public class LogoutRequestContext
+            extends BasicSAMLMessageContext<LogoutResponse, LogoutRequest, NameIDImpl> {
+
+        EntityEnclosingMethod postMethod;
+
+        EntityEnclosingMethod getPostMethod() {
+            return postMethod;
+        }
+
+        void setPostMethod(EntityEnclosingMethod postMethod) {
+            this.postMethod = postMethod;
+        }
+
+        boolean execute(HttpConnection conn) throws HttpException,
+                IOException {
+            return postMethod.execute(new HttpState(), conn) == HttpStatus.SC_OK;
+        }
+
+        String getHttpStatus() {
+            return postMethod.getStatusCode() + " " + postMethod.getStatusText();
+        }
+
+        void releaseConnection() {
+            if (postMethod != null) {
+                postMethod.releaseConnection();
+            }
+        }
+    }
+}
index a43e5c8..6ed4f30 100644 (file)
@@ -77,9 +77,11 @@ import edu.internet2.middleware.shibboleth.common.relyingparty.ProfileConfigurat
 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.SSOConfiguration;
+import edu.internet2.middleware.shibboleth.common.session.SessionManager;
 import edu.internet2.middleware.shibboleth.common.util.HttpHelper;
 import edu.internet2.middleware.shibboleth.idp.authn.PassiveAuthenticationException;
 import edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext;
+import edu.internet2.middleware.shibboleth.idp.session.impl.ServiceInformationImpl;
 import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
 import edu.internet2.middleware.shibboleth.idp.session.Session;
 import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
@@ -281,6 +283,20 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
             }
 
             samlResponse = buildResponse(requestContext, "urn:oasis:names:tc:SAML:2.0:cm:bearer", statements);
+
+            //bind nameID to session.servicesInformation
+            NameID nameID = buildNameId(requestContext);
+            Session session =
+                    getUserSession(requestContext.getInboundMessageTransport());
+            ServiceInformationImpl serviceInfo =
+                    (ServiceInformationImpl) session.getServicesInformation().get(requestContext.getPeerEntityId());
+            serviceInfo.setSAML2NameIdentifier(nameID);
+            //index session by nameid
+            SessionManager<Session> sessionManager = getSessionManager();
+            String index = sessionManager.getIndexFromNameID(nameID);
+            if (index != null) {
+                sessionManager.indexSession(session, index);
+            }
         } catch (ProfileException e) {
             if (requestContext.isUnsolicited()) {
                 // Just delegate to the IdP's global error handler
@@ -777,4 +793,4 @@ public class SSOProfileHandler extends AbstractSAML2ProfileHandler {
         }
 
     }
-}
\ No newline at end of file
+}
index 6779ac4..e4b15f3 100644 (file)
@@ -44,4 +44,32 @@ public interface ServiceInformation extends Serializable {
      * @return authentication method used to log into the service
      */
     public AuthenticationMethodInformation getAuthenticationMethod();
+
+    /**
+     * Gets the principal name identifier for the service.
+     *
+     * @return name identifier
+     */
+    public String getNameIdentifier();
+
+    /**
+     * Gets the principal name identifier format.
+     * 
+     * @return name identifier format
+     */
+    public String getNameIdentifierFormat();
+
+    /**
+     * Gets the name qualifier for the name identifier.
+     *
+     * @return name qualifier
+     */
+    public String getNameQualifier();
+
+    /**
+     * Gets the SP name qualifier for the name identifier.
+     *
+     * @return SP name qualifier
+     */
+    public String getSPNameQualifier();
 }
\ No newline at end of file
index c4b0e0e..25b03b7 100644 (file)
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package edu.internet2.middleware.shibboleth.idp.session.impl;
 
 import org.joda.time.DateTime;
@@ -22,21 +21,28 @@ import org.joda.time.chrono.ISOChronology;
 
 import edu.internet2.middleware.shibboleth.idp.session.AuthenticationMethodInformation;
 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
+import org.opensaml.saml1.core.NameIdentifier;
+import org.opensaml.saml2.core.NameID;
 
 /** Information about a service a user has logged in to. */
 public class ServiceInformationImpl implements ServiceInformation {
-    
-    /** Serial version UID. */
-    private static final long serialVersionUID = 1185342879825302743L;
 
+    /** Serial version UID. */
+    private static final long serialVersionUID = -4284509878936885637L;
     /** Entity ID of the service. */
     private String entityID;
-
     /** Instant the user was authenticated to the service. */
     private long authenticationInstant;
-
     /** Authentication method used to authenticate the user to the service. */
     private AuthenticationMethodInformation methodInfo;
+    /** Name identifier used to identify the user at the service. */
+    private String nameIdentifier;
+    /** Name identifier format. */
+    private String nameIdentifierFormat;
+    /** SP Name qualifier for the name identifier. */
+    private String SPNameQualifier;
+    /** Name qualifier for the name identifier. */
+    private String nameQualifier;
 
     /**
      * Default constructor.
@@ -47,7 +53,8 @@ public class ServiceInformationImpl implements ServiceInformation {
      */
     public ServiceInformationImpl(String id, DateTime loginInstant, AuthenticationMethodInformation method) {
         entityID = id;
-        authenticationInstant = loginInstant.toDateTime(ISOChronology.getInstanceUTC()).getMillis();
+        authenticationInstant =
+                loginInstant.toDateTime(ISOChronology.getInstanceUTC()).getMillis();
         methodInfo = method;
     }
 
@@ -84,4 +91,51 @@ public class ServiceInformationImpl implements ServiceInformation {
         ServiceInformation si = (ServiceInformation) obj;
         return entityID.equals(si.getEntityID());
     }
-}
\ No newline at end of file
+
+    /**
+     * Sets the name identifier for the principal known by the service.
+     * 
+     * @param nameIdentifier
+     */
+    public synchronized void setSAML2NameIdentifier(NameID nameIdentifier) {
+        if (nameIdentifier != null) {
+            this.nameIdentifier = nameIdentifier.getValue();
+            this.nameIdentifierFormat = nameIdentifier.getFormat();
+            this.nameQualifier = nameIdentifier.getNameQualifier();
+            this.SPNameQualifier = nameIdentifier.getSPNameQualifier();
+        }
+    }
+
+    /**
+     * Sets the name identifier for the principal known by the service.
+     * 
+     * @param nameIdentifier
+     */
+    public synchronized void setShibbolethNameIdentifier(NameIdentifier nameIdentifier) {
+        if (nameIdentifier != null) {
+            this.nameIdentifier = nameIdentifier.getNameIdentifier();
+            this.nameIdentifierFormat = nameIdentifier.getFormat();
+            this.nameQualifier = nameIdentifier.getNameQualifier();
+        }
+    }
+
+    /** {@inheritDoc} */
+    public synchronized String getNameIdentifier() {
+        return nameIdentifier;
+    }
+
+    /** {@inheritDoc} */
+    public synchronized String getNameIdentifierFormat() {
+        return nameIdentifierFormat;
+    }
+
+    /** {@inheritDoc} */
+    public synchronized String getNameQualifier() {
+        return nameQualifier;
+    }
+
+    /** {@inheritDoc} */
+    public synchronized String getSPNameQualifier() {
+        return SPNameQualifier;
+    }
+}
index 210405c..3d13523 100644 (file)
@@ -20,6 +20,8 @@ package edu.internet2.middleware.shibboleth.idp.session.impl;
 import java.security.SecureRandom;
 
 import org.apache.commons.ssl.util.Hex;
+import org.opensaml.saml1.core.NameIdentifier;
+import org.opensaml.saml2.core.NameID;
 import org.opensaml.util.storage.StorageService;
 import org.opensaml.xml.util.DatatypeHelper;
 import org.slf4j.Logger;
@@ -90,8 +92,10 @@ public class SessionManagerImpl implements SessionManager<Session> {
         byte[] sessionSecret = new byte[16];
         prng.nextBytes(sessionSecret);
 
-        Session session = new SessionImpl(sessionID, sessionSecret, sessionLifetime);
-        SessionManagerEntry sessionEntry = new SessionManagerEntry(session, sessionLifetime);
+        Session session =
+                new SessionImpl(sessionID, sessionSecret, sessionLifetime);
+        SessionManagerEntry sessionEntry =
+                new SessionManagerEntry(session, sessionLifetime);
         sessionStore.put(partition, sessionID, sessionEntry);
 
         MDC.put("idpSessionId", sessionID);
@@ -109,10 +113,12 @@ public class SessionManagerImpl implements SessionManager<Session> {
         byte[] sessionSecret = new byte[16];
         prng.nextBytes(sessionSecret);
 
-        Session session = new SessionImpl(sessionID, sessionSecret, sessionLifetime);
-        SessionManagerEntry sessionEntry = new SessionManagerEntry(session, sessionLifetime);
+        Session session =
+                new SessionImpl(sessionID, sessionSecret, sessionLifetime);
+        SessionManagerEntry sessionEntry =
+                new SessionManagerEntry(session, sessionLifetime);
         sessionStore.put(partition, sessionID, sessionEntry);
-        
+
         MDC.put("idpSessionId", sessionID);
         log.trace("Created session {}", sessionID);
         return session;
@@ -182,4 +188,43 @@ public class SessionManagerImpl implements SessionManager<Session> {
             sessionEntry.getSessionIndexes().remove(index);
         }
     }
-}
\ No newline at end of file
+
+    /** {@inheritDoc} */
+    public String getIndexFromNameID(NameIdentifier nameIdentifier) {
+        if (nameIdentifier == null || nameIdentifier.getNameIdentifier() == null) {
+            return null;
+        }
+        StringBuilder b = new StringBuilder();
+        b.append(nameIdentifier.getNameIdentifier());
+        b.append("|");
+        b.append(nameIdentifier.getFormat());
+        if (nameIdentifier.getNameQualifier() != null) {
+            b.append("|");
+            b.append(nameIdentifier.getNameQualifier());
+        }
+
+        return b.toString();
+    }
+
+    /** {@inheritDoc} */
+    public String getIndexFromNameID(NameID nameIdentifier) {
+        if (nameIdentifier == null || nameIdentifier.getValue() == null) {
+            return null;
+        }
+        StringBuilder b = new StringBuilder();
+        b.append(nameIdentifier.getValue());
+        b.append("|");
+        b.append(nameIdentifier.getFormat());
+        if (nameIdentifier.getNameQualifier() != null || nameIdentifier.getSPNameQualifier() != null) {
+            b.append("|");
+            b.append(nameIdentifier.getNameQualifier());
+        }
+        if (nameIdentifier.getSPNameQualifier() != null) {
+            b.append("|");
+            b.append(nameIdentifier.getSPNameQualifier());
+        }
+
+        return b.toString();
+    }
+}
+
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/HTTPClientInTransportAdapter.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/HTTPClientInTransportAdapter.java
new file mode 100644 (file)
index 0000000..e416cf5
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import org.apache.commons.httpclient.HttpConnection;
+import org.apache.commons.httpclient.HttpMethod;
+import org.opensaml.ws.transport.http.HTTPInTransport;
+import org.opensaml.ws.transport.http.HTTPTransport.HTTP_VERSION;
+import org.opensaml.xml.security.credential.Credential;
+
+/**
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class HTTPClientInTransportAdapter implements HTTPInTransport {
+
+    private HttpConnection connection;
+    private HttpMethod method;
+
+    public HTTPClientInTransportAdapter(HttpConnection connection, HttpMethod method) {
+        this.connection = connection;
+        this.method = method;
+    }
+
+    public String getPeerAddress() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public String getPeerDomainName() {
+        return connection.getHost();
+    }
+
+    public InputStream getIncomingStream() {
+        try {
+            return method.getResponseBodyAsStream();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Object getAttribute(String name) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public String getCharacterEncoding() {
+        return "utf-8"; //TODO adapt to HttpMethod.getHeader()
+    }
+
+    public Credential getLocalCredential() {
+        return null;
+    }
+
+    public Credential getPeerCredential() {
+        return null;
+    }
+
+    public boolean isAuthenticated() {
+        //TODO support transport authentication?
+        return false;
+    }
+
+    public void setAuthenticated(boolean isAuthenticated) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public boolean isConfidential() {
+        return connection.isSecure();
+    }
+
+    public void setConfidential(boolean isConfidential) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public boolean isIntegrityProtected() {
+        return connection.isSecure();
+    }
+
+    public void setIntegrityProtected(boolean isIntegrityProtected) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public String getHeaderValue(String name) {
+        return method.getResponseHeader(name).getValue();
+    }
+
+    public String getHTTPMethod() {
+        return method.getName();
+    }
+
+    public int getStatusCode() {
+        return method.getStatusCode();
+    }
+
+    public String getParameterValue(String name) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public List<String> getParameterValues(String name) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public HTTP_VERSION getVersion() {
+        return HTTP_VERSION.HTTP1_1;
+    }
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/HTTPClientOutTransportAdapter.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/HTTPClientOutTransportAdapter.java
new file mode 100644 (file)
index 0000000..c3a7332
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import org.apache.commons.httpclient.HttpConnection;
+import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.RequestEntity;
+import org.opensaml.ws.transport.http.HTTPOutTransport;
+import org.opensaml.ws.transport.http.HTTPTransport.HTTP_VERSION;
+import org.opensaml.xml.security.credential.Credential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class HTTPClientOutTransportAdapter implements HTTPOutTransport {
+    private static final Logger log = LoggerFactory.getLogger(HTTPClientOutTransportAdapter.class);
+    
+    private HttpConnection connection;
+    private EntityEnclosingMethod method;
+    SOAPRequestEntity requestEntity;
+
+    public HTTPClientOutTransportAdapter(HttpConnection connection, EntityEnclosingMethod method) {
+        this.connection = connection;
+        this.method = method;
+        requestEntity = new SOAPRequestEntity();
+        method.setRequestEntity(requestEntity);
+    }
+
+    public void setVersion(HTTP_VERSION version) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public void setHeader(String name, String value) {
+        method.addRequestHeader(name, value);
+    }
+
+    public void addParameter(String name, String value) {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not store parameters.");
+    }
+
+    public void setStatusCode(int code) {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not set status code.");
+    }
+
+    public void sendRedirect(String location) {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not send redirect.");
+    }
+
+    public void setAttribute(String name, Object value) {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not store attributes.");
+    }
+
+    public void setCharacterEncoding(String encoding) {
+        requestEntity.setEncoding(encoding);
+    }
+
+    public OutputStream getOutgoingStream() {
+        return requestEntity.getContentStream();
+    }
+
+    public Object getAttribute(String name) {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not store attributes.");
+    }
+
+    public String getCharacterEncoding() {
+        return requestEntity.getEncoding();
+    }
+
+    public Credential getLocalCredential() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public Credential getPeerCredential() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public boolean isAuthenticated() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public void setAuthenticated(boolean isAuthenticated) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public boolean isConfidential() {
+        return connection.isSecure();
+    }
+
+    public void setConfidential(boolean isConfidential) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public boolean isIntegrityProtected() {
+        return connection.isSecure();
+    }
+
+    public void setIntegrityProtected(boolean isIntegrityProtected) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public String getHeaderValue(String name) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    public String getHTTPMethod() {
+        return "POST";
+    }
+
+    public int getStatusCode() {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not support Status code.");
+    }
+
+    public String getParameterValue(String name) {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not support Parameters.");
+    }
+
+    public List<String> getParameterValues(String name) {
+        throw new UnsupportedOperationException("This is an HTTP Client, can not support Parameters.");
+    }
+
+    public HTTP_VERSION getVersion() {
+        return HTTP_VERSION.HTTP1_1;
+    }
+
+
+    class SOAPRequestEntity implements RequestEntity {
+        private ByteArrayOutputStream contentStream = new ByteArrayOutputStream();
+        private String encoding = "utf-8";
+
+        public boolean isRepeatable() {
+            return false;
+        }
+
+        public void writeRequest(OutputStream out) throws IOException {
+            out.write(contentStream.toByteArray());
+        }
+
+        public long getContentLength() {
+            return contentStream.size();
+        }
+
+        public String getContentType() {
+            return "text/xml; encoding=" + encoding;
+        }
+
+        void setEncoding(String encoding) {
+            this.encoding = encoding;
+        }
+
+        String getEncoding() {
+            return encoding;
+        }
+
+        OutputStream getContentStream() {
+            return contentStream;
+        }
+    }
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/LogoutServlet.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/LogoutServlet.java
new file mode 100644 (file)
index 0000000..f8abf19
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import edu.internet2.middleware.shibboleth.idp.profile.saml2.SLOProfileHandler;
+import java.io.IOException;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.opensaml.xml.util.DatatypeHelper;
+
+/**
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class LogoutServlet extends HttpServlet {
+
+    private static final long serialVersionUID = -7808054647676905576L;
+    /**
+     * Front-channel single logout profile handler path.
+     */
+    private String profileHandlerPath;
+
+    @Override
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+        profileHandlerPath = config.getInitParameter("profileHandlerPath");
+        if (DatatypeHelper.isEmpty(profileHandlerPath)) {
+            throw new ServletException("Required parameter 'profileHandlerPath' is not set.");
+        }
+    }
+
+    @Override
+    protected void service(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+        req.setAttribute(SLOProfileHandler.IDP_INITIATED_LOGOUT_ATTR, true);
+        req.getRequestDispatcher(profileHandlerPath).forward(req, resp);
+    }
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SLOContextFilter.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SLOContextFilter.java
new file mode 100644 (file)
index 0000000..7f3edbc
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
+import edu.internet2.middleware.shibboleth.idp.authn.LoginContextEntry;
+import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.opensaml.util.storage.StorageService;
+
+/**
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class SLOContextFilter implements Filter {
+
+    // TODO remove once HttpServletHelper does redirects
+    private static ServletContext context;
+    /** Storage service used to store {@link LoginContext}s while authentication is in progress. */
+    private static StorageService<String, LoginContextEntry> storageService;
+
+    /** {@inheritDoc} */
+    public void init(FilterConfig config) throws ServletException {
+        storageService =
+                (StorageService<String, LoginContextEntry>) HttpServletHelper.getStorageService(config.getServletContext());
+        context = config.getServletContext();
+    }
+
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        HttpServletRequest req = (HttpServletRequest) request;
+        HttpServletResponse resp = (HttpServletResponse) response;
+        
+        SingleLogoutContext sloContext =
+                SingleLogoutContextStorageHelper.getSingleLogoutContext(req);
+        if (sloContext != null) {
+            //context found in the request, this must be a forward
+            SingleLogoutContextStorageHelper.bindSingleLogoutContext(sloContext, storageService, context, req, resp);
+        } else {
+            sloContext =
+                    SingleLogoutContextStorageHelper.getSingleLogoutContext(storageService, context, req);
+            SingleLogoutContextStorageHelper.bindSingleLogoutContext(sloContext, req);
+        }
+
+        chain.doFilter(request, response);
+    }
+
+    public void destroy() {
+    }
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SLOServlet.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SLOServlet.java
new file mode 100644 (file)
index 0000000..cb690c6
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
+import edu.internet2.middleware.shibboleth.idp.authn.LoginContextEntry;
+import edu.internet2.middleware.shibboleth.idp.profile.saml2.SLOProfileHandler;
+import edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext.LogoutInformation;
+import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Iterator;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.opensaml.util.storage.StorageService;
+
+/**
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class SLOServlet extends HttpServlet {
+
+    private static final long serialVersionUID = -3562061733288921508L;
+    // TODO remove once HttpServletHelper does redirects
+    private static ServletContext context;
+    /** Storage service used to store {@link LoginContext}s while authentication is in progress. */
+    private static StorageService<String, LoginContextEntry> storageService;
+
+    /** {@inheritDoc} */
+    @Override
+    public void init(ServletConfig config) throws ServletException {
+        super.init(config);
+
+        storageService =
+                (StorageService<String, LoginContextEntry>) HttpServletHelper.getStorageService(config.getServletContext());
+        context = config.getServletContext();
+    }
+
+    @Override
+    protected void service(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+
+        SingleLogoutContext sloContext =
+                SingleLogoutContextStorageHelper.getSingleLogoutContext(req);
+        if (sloContext == null) {
+            //remove stale cookie if exists
+            SingleLogoutContextStorageHelper.removeSingleLogoutContextCookie(req, resp);
+            resp.sendError(404, "Single Logout servlet can not be called directly");
+            return;
+        }
+        resp.setHeader("Cache-Control", "no-cache, must-revalidate");
+        resp.setHeader("Pragma", "no-cache");
+
+        if (req.getParameter("status") != null) { //status query, response is JSON
+            sloContext.checkTimeout();
+            PrintWriter writer = resp.getWriter();
+            writer.print("[");
+            Iterator<SingleLogoutContext.LogoutInformation> it =
+                    sloContext.getServiceInformation().values().iterator();
+            while (it.hasNext()) {
+                LogoutInformation service = it.next();
+                writer.print("{\"entityID\":\"");
+                writer.print(service.getEntityID());
+                writer.print("\",\"logoutStatus\":\"");
+                writer.print(service.getLogoutStatus().toString());
+                writer.print("\"}");
+                if (it.hasNext()) {
+                    writer.print(",");
+                }
+            }
+            writer.print("]");
+        } else if (req.getParameter("action") != null) { //forward to handler
+            req.getRequestDispatcher(sloContext.getProfileHandlerURL()).forward(req, resp);
+        } else if (req.getParameter("finish") != null) { //forward to handler
+            SingleLogoutContextStorageHelper.unbindSingleLogoutContext(storageService, context, req, resp);
+            req.getRequestDispatcher(sloContext.getProfileHandlerURL()).forward(req, resp);
+        } else if (req.getParameter("logout") != null || 
+                req.getAttribute(SLOProfileHandler.SKIP_LOGOUT_QUESTION_ATTR) != null) {
+            //respond with SLO Controller
+            sloContext.checkTimeout();
+            req.getRequestDispatcher("/sloController.jsp").forward(req, resp);
+        } else { //respond with confirmation dialog
+            req.getRequestDispatcher("/sloQuestion.jsp").forward(req, resp);
+        }
+    }
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContext.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContext.java
new file mode 100644 (file)
index 0000000..59341e7
--- /dev/null
@@ -0,0 +1,485 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.LogoutRequestConfiguration;
+import edu.internet2.middleware.shibboleth.idp.profile.saml2.SLOProfileHandler.InitialLogoutRequestContext;
+import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
+import edu.internet2.middleware.shibboleth.idp.session.Session;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Context object for storing information associated with a Single Logout
+ * event.
+ *
+ * The SingleLogoutContext holds all information of the SAML LogoutRequest
+ * which is needed to respond (RelayState, ID, EntityID). SingleLogoutContext
+ * also stores the status of the Logout for each session participant.
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class SingleLogoutContext implements Serializable {
+
+    private static final long serialVersionUID = -2386684678331311278L;
+    /** EntityID of the entity which requested the logout. */
+    private final String requesterEntityID;
+    /** EntityID of the IdP which will respond. */
+    private final String responderEntityID;
+    /** SAML ID of the LogoutRequest. */
+    private final String requestSAMLMessageID;
+    /** RelayState of the LogoutRequest. */
+    private final String relayState;
+    /** URL of the current profile handler. */
+    private final String profileHandlerURL;
+    /** Internal IdP Session ID. */
+    private final String idpSessionID;
+    /** Timeout value for frontchannel requests (in milliseconds). */
+    private final int frontChannelResponseTimeout;
+    /** Logout information associated with each session participant. */
+    private final Map<String, LogoutInformation> serviceInformation;
+
+    /**
+     * Private constructor to create a new SingleLogoutContext instance.
+     * 
+     * @param profileHandlerURL URL for the SLO profile handler
+     * @param requesterEntityID entityID of the requester SP (may be null)
+     * @param responderEntityID entityID of the IdP
+     * @param requestSAMLMessageID ID of the SAML LogoutRequest message
+     * @param relayState RelayState associated with the LogoutRequest
+     * @param profileConfiguration Single Logout profile configuration
+     * @param idpSession IdP session for the principal
+     */
+    private SingleLogoutContext(
+            String profileHandlerURL,
+            String requesterEntityID,
+            String responderEntityID,
+            String requestSAMLMessageID,
+            String relayState,
+            LogoutRequestConfiguration profileConfiguration,
+            Session idpSession) {
+
+        this.profileHandlerURL = profileHandlerURL;
+        this.requesterEntityID = requesterEntityID;
+        this.responderEntityID = responderEntityID;
+        this.requestSAMLMessageID = requestSAMLMessageID;
+        this.relayState = relayState;
+        this.frontChannelResponseTimeout =
+                profileConfiguration.getFrontChannelResponseTimeout();
+        this.idpSessionID = idpSession.getSessionID();
+
+        Map<String, ServiceInformation> serviceInformationMap =
+                idpSession.getServicesInformation();
+        Map<String, LogoutInformation> serviceInfo =
+                new HashMap<String, LogoutInformation>(serviceInformationMap.size());
+        for (ServiceInformation service : serviceInformationMap.values()) {
+            LogoutInformation logoutInfo;
+            if (!service.getEntityID().equals(requesterEntityID)) {
+                logoutInfo =
+                        new LogoutInformation(service, LogoutStatus.LOGGED_IN);
+            } else {
+                logoutInfo =
+                        new LogoutInformation(service, LogoutStatus.LOGOUT_SUCCEEDED);
+            }
+            serviceInfo.put(service.getEntityID(), logoutInfo);
+        }
+        this.serviceInformation = Collections.unmodifiableMap(serviceInfo);
+    }
+
+    /**
+     * Create a new instance of SingleLogoutContext.
+     * 
+     * @param profileHandlerURL URL of the SLO profile handler
+     * @param initialRequest initial logout request
+     * @param idpSession IdP session for the principal
+     * @return
+     */
+    public static SingleLogoutContext createInstance(
+            String profileHandlerURL,
+            InitialLogoutRequestContext initialRequest,
+            Session idpSession) {
+
+        return new SingleLogoutContext(
+                profileHandlerURL,
+                initialRequest.getPeerEntityId(),
+                initialRequest.getLocalEntityId(),
+                initialRequest.getInboundSAMLMessageId(),
+                initialRequest.getRelayState(),
+                initialRequest.getProfileConfiguration(),
+                idpSession);
+    }
+
+    /**
+     * Returns the RelayState of the initial SAML LogoutRequest (if any).
+     * 
+     * @return RelayState or NULL
+     */
+    public String getRelayState() {
+        return relayState;
+    }
+
+    /**
+     * Returns the ID of the initial SAML LogoutRequest message.
+     *
+     * @return message ID or NULL if the logout is IdP-initiated
+     */
+    public String getRequestSAMLMessageID() {
+        return requestSAMLMessageID;
+    }
+
+    /**
+     * Returns the entityID of the requesting session participant.
+     *
+     * @return entityID or NULL if the logout is IdP-initiated
+     */
+    public String getRequesterEntityID() {
+        return requesterEntityID;
+    }
+
+    /**
+     * Returns the entityID of the IdP itself.
+     *
+     * @return entityID of the IdP
+     */
+    public String getResponderEntityID() {
+        return responderEntityID;
+    }
+
+    /**
+     * Returns the Single Logout profile handler URL which must be invoked when
+     * the Logout process is finished.
+     * 
+     * @return profile handler URL
+     */
+    public String getProfileHandlerURL() {
+        return profileHandlerURL;
+    }
+
+    /**
+     * Returns the session ID of the principal.
+     *
+     * @return session ID
+     */
+    public String getIdpSessionID() {
+        return idpSessionID;
+    }
+
+    /**
+     * Returns the logout information associated with each session participant.
+     *
+     * @return logout information for session participants
+     */
+    public Map<String, LogoutInformation> getServiceInformation() {
+        return serviceInformation;
+    }
+
+    /**
+     * Checks all services for logout timeout. This method must be called
+     * before the front channel logout status is determined and returned back
+     * to the client.
+     */
+    public synchronized void checkTimeout() {
+        for (LogoutInformation serviceLogoutInfo : serviceInformation.values()) {
+            if (serviceLogoutInfo.getLogoutStatus().equals(LogoutStatus.LOGOUT_ATTEMPTED)
+                    && serviceLogoutInfo.getElapsedMillis()
+                    > frontChannelResponseTimeout) {
+                serviceLogoutInfo.setLogoutTimedOut();
+            }
+        }
+    }
+
+    /**
+     * Logout Status for a session participant.
+     */
+    public enum LogoutStatus implements Serializable {
+
+        LOGGED_IN, LOGOUT_ATTEMPTED, LOGOUT_SUCCEEDED, LOGOUT_FAILED,
+        LOGOUT_UNSUPPORTED, LOGOUT_TIMED_OUT
+    }
+
+    /**
+     * Class for holding all information associated with a session participant
+     * during the single logout process.
+     *
+     * @author Adam Lantos  NIIF / HUNGARNET
+     */
+    public class LogoutInformation implements Serializable {
+
+        private static final long serialVersionUID = -1371240803047042613L;
+        /** entityID of the SP */
+        private final String entityID;
+        /** name identifier value issued for the principal to this SP. */
+        private final String nameIdentifier;
+        /** name identifier format. */
+        private final String nameIdentifierFormat;
+        /** qualifier for the name identifier. */
+        private final String nameQualifier;
+        /** SP name qualifier for the name identifier (SAML2 case only). */
+        private final String SPNameQualifier;
+        /** status of the logout process. */
+        private LogoutStatus logoutStatus;
+        /** SAML logout request ID. */
+        private String logoutRequestId;
+        /** SP display names from the Metadata. */
+        private Map<String, String> displayName;
+        /** timestamp of the logout request. */
+        private long logoutTimestamp;
+
+        /**
+         * Creates new LogoutInformation instance.
+         *
+         * @param entityID SP entityID
+         * @param nameIdentifier identifier value issued for the principal
+         * @param nameIdentifierFormat name identifier format
+         * @param nameQualifier name qualifier
+         * @param SPNameQualifier SP name qualifier
+         * @param logoutStatus status of the logout
+         */
+        LogoutInformation(String entityID, String nameIdentifier,
+                String nameIdentifierFormat, String nameQualifier,
+                String SPNameQualifier, LogoutStatus logoutStatus) {
+
+            this.entityID = entityID;
+            this.nameIdentifier = nameIdentifier;
+            this.nameIdentifierFormat = nameIdentifierFormat;
+            this.nameQualifier = nameQualifier;
+            this.SPNameQualifier = SPNameQualifier;
+            this.logoutStatus = logoutStatus;
+        }
+
+        /**
+         * Creates new LogoutInformation instance.
+         *
+         * @param service session participant service information
+         * @param status status of the logout
+         */
+        LogoutInformation(ServiceInformation service, LogoutStatus status) {
+            this(service.getEntityID(), service.getNameIdentifier(),
+                    service.getNameIdentifierFormat(), service.getNameQualifier(),
+                    service.getSPNameQualifier(), status);
+        }
+
+        /**
+         * Returns the entityID of the session participant.
+         *
+         * @return entityID
+         */
+        public String getEntityID() {
+            return entityID;
+        }
+
+        /**
+         * Returns the status of the logout.
+         *
+         * @return status of the logout.
+         */
+        public synchronized LogoutStatus getLogoutStatus() {
+            return logoutStatus;
+        }
+
+        /**
+         * Sets status of the logout.
+         *
+         * @param logoutStatus status of the logout
+         */
+        private void setLogoutStatus(LogoutStatus logoutStatus) {
+            this.logoutStatus = logoutStatus;
+        }
+
+        /**
+         * Sets status to LOGOUT_UNSUPPORTED.
+         * @throws IllegalStateException  when logout status is not LOGGED_IN.
+         */
+        public synchronized void setLogoutUnsupported() {
+            if (getLogoutStatus().equals(LogoutStatus.LOGGED_IN)) {
+                this.setLogoutStatus(LogoutStatus.LOGOUT_UNSUPPORTED);
+            } else {
+                throw new IllegalStateException("LogoutStatus is not LOGGED_IN");
+            }
+        }
+
+        /**
+         * Sets status to LOGOUT_ATTEMPTED.
+         * @throws IllegalStateException when logout status is not LOGGED_IN.
+         */
+        public synchronized void setLogoutAttempted() {
+            if (getLogoutStatus().equals(LogoutStatus.LOGGED_IN)) {
+                this.setLogoutStatus(LogoutStatus.LOGOUT_ATTEMPTED);
+                this.logoutTimestamp = System.currentTimeMillis();
+            } else {
+                throw new IllegalStateException("Logout already attempted");
+            }
+        }
+
+        /**
+         * Sets status to LOGOUT_FAILED.
+         * @throws IllegalStateException when logout status is not LOGOUT_ATTEMPTED.
+         */
+        public synchronized void setLogoutFailed() {
+            if (getLogoutStatus().equals(LogoutStatus.LOGOUT_ATTEMPTED)) {
+                this.setLogoutStatus(LogoutStatus.LOGOUT_FAILED);
+            } else {
+                throw new IllegalStateException("LogoutStatus is not LOGOUT_ATTEMPTED");
+            }
+        }
+
+        /**
+         * Sets status to LOGOUT_SUCCEEDED.
+         * @throws IllegalStateException when logout status is not LOGOUT_ATTEMPTED.
+         */
+        public synchronized void setLogoutSucceeded() {
+            if (getLogoutStatus().equals(LogoutStatus.LOGOUT_ATTEMPTED)) {
+                this.setLogoutStatus(LogoutStatus.LOGOUT_SUCCEEDED);
+            } else {
+                throw new IllegalStateException("LogoutStatus is not LOGOUT_ATTEMPTED");
+            }
+        }
+
+        /**
+         * Sets status to LOGOUT_TIMED_OUT.
+         * @throws IllegalStateException when logout status is not LOGOUT_ATTEMPTED.
+         */
+        public synchronized void setLogoutTimedOut() {
+            if (getLogoutStatus().equals(LogoutStatus.LOGOUT_ATTEMPTED)) {
+                this.setLogoutStatus(LogoutStatus.LOGOUT_TIMED_OUT);
+            } else {
+                throw new IllegalStateException("LogoutStatus is not LOGOUT_ATTEMPTED");
+            }
+        }
+
+        /**
+         * Returns whether this service is still in LOGGED_IN state.
+         * @return
+         */
+        public boolean isLoggedIn() {
+            return getLogoutStatus().equals(LogoutStatus.LOGGED_IN);
+        }
+
+        /**
+         * Returns the name identifier for the principal at this SP.
+         * @return name identifier
+         */
+        public String getNameIdentifier() {
+            return nameIdentifier;
+        }
+
+        /**
+         * Returns name identifier format for the principal at this SP.
+         *
+         * @return name identifier format
+         */
+        public String getNameIdentifierFormat() {
+            return nameIdentifierFormat;
+        }
+
+        /**
+         * Returns the name qualifier for the principal at this SP.
+         *
+         * @return name qualifier or NULL
+         */
+        public String getNameQualifier() {
+            return nameQualifier;
+        }
+
+        /**
+         * Returns the SP name qualifier for the principal at this SP.
+         *
+         * @return SP name qualifier or NULL
+         */
+        public String getSPNameQualifier() {
+            return SPNameQualifier;
+        }
+
+        /**
+         * Returns the ID of the SAML logout request.
+         *
+         * @return SAML logout request ID
+         */
+        public synchronized String getLogoutRequestId() {
+            return logoutRequestId;
+        }
+
+        /**
+         * Sets the SAML logout request ID. This method must be called once.
+         *
+         * @param logoutRequestId SAML logout request ID
+         */
+        public synchronized void setLogoutRequestId(String logoutRequestId) {
+            if (this.logoutRequestId == null) {
+                this.logoutRequestId = logoutRequestId;
+            } else {
+                throw new IllegalStateException("Request ID is previously set");
+            }
+        }
+
+        /**
+         * Sets localized display names for this SP. This method must be called once.
+         *
+         * @param displayName map of language and display name
+         */
+        public synchronized void setDisplayName(Map<String, String> displayName) {
+            if (this.displayName == null) {
+                this.displayName = Collections.unmodifiableMap(displayName);
+            } else {
+                throw new IllegalStateException("Display Name is previously set");
+            }
+        }
+
+        /**
+         * Returns localized display name for the SP.
+         * If no display name is found for the given languages, the entityID
+         * is returned.
+         *
+         * @param locale locale defined by the user agent
+         * @param defaultLocale default fallback locale
+         * @return localized display name or the entityID
+         */
+        public String getDisplayName(Locale locale, Locale defaultLocale) {
+            String dName = null;
+            if (displayName != null) {
+                if (locale != null) {
+                    dName = displayName.get(locale.getLanguage());
+                }
+                if (dName == null && defaultLocale != null) {
+                    dName = displayName.get(defaultLocale.getLanguage());
+                }
+            }
+            if (dName == null) {
+                dName = entityID;
+            }
+
+            return dName;
+        }
+
+        /**
+         * Returns the elapsed milliseconds since the logout request is sent.
+         * This method must only be called when the request timestamp is set.
+         *
+         * @return elapsed millisenconds since logout initiation
+         */
+        long getElapsedMillis() {
+            if (this.logoutTimestamp == 0) {
+                throw new IllegalStateException("Logout timestamp is not initialized");
+            }
+
+            return System.currentTimeMillis() - logoutTimestamp;
+        }
+    }
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContextEntry.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContextEntry.java
new file mode 100644 (file)
index 0000000..06c1c4f
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import org.joda.time.DateTime;
+import org.opensaml.util.storage.AbstractExpiringObject;
+
+/**
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class SingleLogoutContextEntry extends AbstractExpiringObject {
+    private static final long serialVersionUID = 8456530807574247919L;
+
+    /** Stored single logout context. */
+    private SingleLogoutContext singleLogoutContext;
+
+    /**
+     * Constructor.
+     *
+     * @param ctx context to store
+     * @param lifetime lifetime of the entry
+     */
+    public SingleLogoutContextEntry(SingleLogoutContext ctx, long lifetime) {
+        super(new DateTime().plus(lifetime));
+        singleLogoutContext = ctx;
+    }
+
+    public SingleLogoutContext getSingleLogoutContext() {
+        return singleLogoutContext;
+    }
+}
diff --git a/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContextStorageHelper.java b/src/main/java/edu/internet2/middleware/shibboleth/idp/slo/SingleLogoutContextStorageHelper.java
new file mode 100644 (file)
index 0000000..a468e8f
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ *  Copyright 2009 NIIF Institute.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package edu.internet2.middleware.shibboleth.idp.slo;
+
+import edu.internet2.middleware.shibboleth.idp.util.HttpServletHelper;
+import java.util.UUID;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.opensaml.util.storage.StorageService;
+import org.opensaml.xml.util.DatatypeHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * @author Adam Lantos  NIIF / HUNGARNET
+ */
+public class SingleLogoutContextStorageHelper {
+
+    /** Name of the key to the current single logout context: {@value} . */
+    public static final String SLO_CTX_KEY_NAME = "_idp_slo_ctx_key";
+    /**
+     * {@link ServletContext} parameter name bearing the name of the {@link StorageService} partition into which
+     * {@link SingleLogoutContext}s are stored: {@value} .
+     */
+    public static final String SLO_CTX_PARTITION_CTX_PARAM =
+            "sloContextPartitionName";
+    /** Default name for the {@link StorageService} partition which holds
+     * {@link SingleLogoutContext}s: {@value} . */
+    public static final String DEFAULT_SLO_CTX_PARTITION = "sloContexts";
+    /** Class logger. */
+    private static final Logger log =
+            LoggerFactory.getLogger(SingleLogoutContextStorageHelper.class);
+
+    /**
+     * Gets the single logout context from the current request. The logout context
+     * is only in this location while the request is being transferred from the
+     * Single logout servlet to the SLO Profile handler.
+     *
+     * @param httpRequest current HTTP request
+     *
+     * @return the single logout context or null if no logout context is bound to the request
+     */
+    public static SingleLogoutContext getSingleLogoutContext(HttpServletRequest httpRequest) {
+        return (SingleLogoutContext) httpRequest.getAttribute(SLO_CTX_KEY_NAME);
+    }
+
+    /**
+     * Gets the {@link SingleLogoutContext} for the user issuing the HTTP request.
+     *
+     * @param context the Servlet context
+     * @param storageService storage service to use when retrieving the login context
+     * @param httpRequest current HTTP request
+     *
+     * @return the single logout context or null if none is available
+     */
+    public static SingleLogoutContext getSingleLogoutContext(StorageService storageService, ServletContext context,
+            HttpServletRequest httpRequest) {
+        if (storageService == null) {
+            throw new IllegalArgumentException("Storage service may not be null");
+        }
+        if (context == null) {
+            throw new IllegalArgumentException("Servlet context may not be null");
+        }
+        if (httpRequest == null) {
+            throw new IllegalArgumentException("HTTP request may not be null");
+        }
+
+        SingleLogoutContext sloContext = getSingleLogoutContext(httpRequest);
+        if (sloContext == null) {
+            log.debug("LoginContext not bound to HTTP request, retrieving it from storage service");
+            Cookie sloContextKeyCookie =
+                    HttpServletHelper.getCookie(httpRequest, SLO_CTX_KEY_NAME);
+            if (sloContextKeyCookie == null) {
+                log.debug("SingleLogoutContext key cookie was not present in request");
+                return null;
+            }
+
+            String sloContextKey =
+                    DatatypeHelper.safeTrimOrNullString(sloContextKeyCookie.getValue());
+            if (sloContextKey == null) {
+                log.warn("Corrupted SingleLogoutContext Key cookie, it did not contain a value");
+            }
+            log.debug("SingleLogoutContext key is '{}'", sloContextKey);
+
+            String partition =
+                    HttpServletHelper.getContextParam(context, SLO_CTX_PARTITION_CTX_PARAM, DEFAULT_SLO_CTX_PARTITION);
+            log.debug("partition: {}", partition);
+            SingleLogoutContextEntry entry =
+                    (SingleLogoutContextEntry) storageService.get(partition, sloContextKey);
+            if (entry != null) {
+                if (entry.isExpired()) {
+                    log.debug("SingleLogoutContext found but it was expired");
+                } else {
+                    sloContext = entry.getSingleLogoutContext();
+                }
+            } else {
+                log.debug("No single logout context in storage service");
+            }
+        }
+
+        return sloContext;
+    }
+
+    /**
+     * Binds a {@link SingleLogoutContext} to the current request.
+     *
+     * @param sloContext login context to be bound
+     * @param request current HTTP request
+     */
+    public static void bindSingleLogoutContext(SingleLogoutContext sloContext, HttpServletRequest httpRequest) {
+        if (httpRequest == null) {
+            throw new IllegalArgumentException("HTTP request may not be null");
+        }
+        httpRequest.setAttribute(SLO_CTX_KEY_NAME, sloContext);
+    }
+
+    /**
+     * Binds a {@link SingleLogoutContext} to the issuer of the current request.
+     * The binding is done by creating a random UUID, placing that in a cookie
+     * in the request, and storing the context in to the storage service under that key.
+     *
+     * @param sloContext the single logout context to be bound
+     * @param storageService the storage service which will hold the context
+     * @param context the Servlet context
+     * @param httpRequest the current HTTP request
+     * @param httpResponse the current HTTP response
+     */
+    public static void bindSingleLogoutContext(SingleLogoutContext sloContext, StorageService storageService,
+            ServletContext context, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+        if (storageService == null) {
+            throw new IllegalArgumentException("Storage service may not be null");
+        }
+        if (httpRequest == null) {
+            throw new IllegalArgumentException("HTTP request may not be null");
+        }
+        if (sloContext == null) {
+            return;
+        }
+
+        bindSingleLogoutContext(sloContext, httpRequest);
+
+        String partition = HttpServletHelper.getContextParam(
+                context, SLO_CTX_PARTITION_CTX_PARAM, DEFAULT_SLO_CTX_PARTITION);
+        log.debug("SingleLogoutContext partition: {}", partition);
+
+        String contextKey = UUID.randomUUID().toString();
+        while (storageService.contains(partition, contextKey)) {
+            contextKey = UUID.randomUUID().toString();
+        }
+        log.debug("SingleLogoutContext key: {}", contextKey);
+
+        SingleLogoutContextEntry entry =
+                new SingleLogoutContextEntry(sloContext, 1800000);
+        storageService.put(partition, contextKey, entry);
+
+        Cookie contextKeyCookie = new Cookie(SLO_CTX_KEY_NAME, contextKey);
+        contextKeyCookie.setPath("/");
+        contextKeyCookie.setSecure(httpRequest.isSecure());
+        httpResponse.addCookie(contextKeyCookie);
+    }
+
+    /**
+     * Unbinds a {@link SingleLogoutContext} from the current request.
+     * The unbinding results in the destruction of the associated context key
+     * cookie and removes the context from the storage service.
+     *
+     * @param storageService storage service holding the context
+     * @param context the Servlet context
+     * @param httpRequest current HTTP request
+     * @param httpResponse current HTTP response
+     *
+     * @return the login context that was unbound or null if there was no bound context
+     */
+    public static SingleLogoutContext unbindSingleLogoutContext(StorageService storageService, ServletContext context,
+            HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+        if (storageService == null || context == null || httpRequest == null ||
+                httpResponse == null) {
+            return null;
+        }
+
+        String sloContextKey = removeSingleLogoutContextCookie(httpRequest, httpResponse);
+        if (sloContextKey == null) {
+            return null;
+        }
+
+        SingleLogoutContextEntry entry =
+                (SingleLogoutContextEntry) storageService.remove(HttpServletHelper.getContextParam(context,
+                SLO_CTX_PARTITION_CTX_PARAM, DEFAULT_SLO_CTX_PARTITION), sloContextKey);
+        if (entry != null && !entry.isExpired()) {
+            return entry.getSingleLogoutContext();
+        }
+        return null;
+    }
+
+    /**
+     * Removes cookie for SingleLogoutContext and returns the logout context key.
+     * 
+     * @param httpRequest
+     * @param httpResponse
+     */
+    protected static String removeSingleLogoutContextCookie(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+        Cookie sloContextKeyCookie =
+                HttpServletHelper.getCookie(httpRequest, SLO_CTX_KEY_NAME);
+        if (sloContextKeyCookie == null) {
+            return null;
+        }
+        String sloContextKey =
+                DatatypeHelper.safeTrimOrNullString(sloContextKeyCookie.getValue());
+        if (sloContextKey == null) {
+            log.warn("Corrupted SingleLogoutContext Key cookie, it did not contain a value");
+        }
+
+        if (sloContextKeyCookie == null) {
+            return null;
+        }
+        sloContextKeyCookie.setMaxAge(0);
+        httpResponse.addCookie(sloContextKeyCookie);
+
+        return sloContextKey;
+    }
+}
index b5fbb0c..5fa2876 100644 (file)
         </xsd:complexContent>
     </xsd:complexType>
 
+    <xsd:complexType name="SAML2SLO">
+        <xsd:annotation>
+            <xsd:documentation>Configuration type for SAML 2 SLO profile handlers.</xsd:documentation>
+        </xsd:annotation>
+        <xsd:complexContent>
+            <xsd:extension base="SAML2ProfileHandler" />
+        </xsd:complexContent>
+    </xsd:complexType>
+
     <xsd:complexType name="SAML2ECP">
         <xsd:annotation>
             <xsd:documentation>Configuration type for ECP SAML 2 SSO profile handlers.</xsd:documentation>
index c1973d2..5b39652 100644 (file)
         <url-pattern>/*</url-pattern>
     </filter-mapping>
 
+    <!--  Add IdP SLO Context object to incoming profile requests -->
+    <filter>
+        <filter-name>SLOContextFilter</filter-name>
+        <filter-class>edu.internet2.middleware.shibboleth.idp.slo.SLOContextFilter</filter-class>
+    </filter>
+
+    <filter-mapping>
+        <filter-name>SLOContextFilter</filter-name>
+        <url-pattern>/profile/SAML2/SOAP/SLO</url-pattern>
+    </filter-mapping>
+    <filter-mapping>
+        <filter-name>SLOContextFilter</filter-name>
+        <url-pattern>/profile/SAML2/Redirect/SLO</url-pattern>
+    </filter-mapping>
+    <filter-mapping>
+        <filter-name>SLOContextFilter</filter-name>
+        <url-pattern>/profile/SAML2/POST/SLO</url-pattern>
+    </filter-mapping>
+    <filter-mapping>
+        <filter-name>SLOContextFilter</filter-name>
+        <url-pattern>/SLOServlet</url-pattern>
+        <dispatcher>REQUEST</dispatcher>
+        <dispatcher>FORWARD</dispatcher>
+    </filter-mapping>
+    <!-- END of SLO Context Filter -->
     <!-- HTTP headers to every response in order to prevent response caching -->
     <filter>
         <filter-name>IdPNoCacheFilter</filter-name>
         <url-pattern>/AuthnEngine</url-pattern>
     </servlet-mapping>
 
+    <!-- SLO Servlet -->
+    <servlet>
+        <servlet-name>SLOServlet</servlet-name>
+        <servlet-class>edu.internet2.middleware.shibboleth.idp.slo.SLOServlet</servlet-class>
+        <load-on-startup>3</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>SLOServlet</servlet-name>
+        <url-pattern>/SLOServlet</url-pattern>
+    </servlet-mapping>
+    
+    <!-- Servlet for IdP - initiated Logout -->
+    <servlet>
+        <servlet-name>LogoutServlet</servlet-name>
+        <servlet-class>edu.internet2.middleware.shibboleth.idp.slo.LogoutServlet</servlet-class>
+        <init-param>
+            <!-- Path for front-channel single logout profile handler -->
+            <param-name>profileHandlerPath</param-name>
+            <param-value>/profile/SAML2/Redirect/SLO</param-value>
+        </init-param>
+        <load-on-startup>3</load-on-startup>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>LogoutServlet</servlet-name>
+        <url-pattern>/Logout</url-pattern>
+    </servlet-mapping>
+
     <!-- Servlet protected by container used for RemoteUser authentication -->
     <servlet>
         <servlet-name>RemoteUserAuthHandler</servlet-name>
diff --git a/src/main/webapp/css/main.css b/src/main/webapp/css/main.css
new file mode 100644 (file)
index 0000000..67411cd
--- /dev/null
@@ -0,0 +1,20 @@
+body {font-size:12px;font-family:arial,helvetica,sans-serif;margin:80px auto 20px auto;width:480px;}
+
+iframe {position:absolute;top:-1000px;}
+
+h1 {font-size:28px;color:#555;padding-bottom:8px;margin:2px 8px;border-bottom:1px solid #a2a2a2;}
+h2 {font-size:20px;color:#555;padding:4px 0;margin:2px 8px;}
+
+form {float:left;padding:5px 10px;margin:0;text-align:center;}
+
+.content {border:2px solid #666;padding:5px;margin:5px;}
+.row {clear:both;padding:6px 14px;font-size:14px; background-image: url('../images/list_image.png');background-repeat: no-repeat;background-position: 2px 12px;         list-style-type: none; margin-left:8px;}
+.row img {padding-left:10px;border:0;width:14px;height:14px;}
+
+.controller {clear:both;padding:5px 8px; font-size:20px; font-weight:bold;background-color:#d2d2d2;margin:10px 0 4px 0;}
+
+#result {clear:both;padding:8px 0 15px 0;font-size:16px;text-align:center;font-weight:bold;}
+.success {color:#159015;}
+.fail {color:red;}
+
+.clear {clear:both;}
diff --git a/src/main/webapp/images/failed.png b/src/main/webapp/images/failed.png
new file mode 100644 (file)
index 0000000..bb555a3
Binary files /dev/null and b/src/main/webapp/images/failed.png differ
diff --git a/src/main/webapp/images/indicator.gif b/src/main/webapp/images/indicator.gif
new file mode 100644 (file)
index 0000000..085ccae
Binary files /dev/null and b/src/main/webapp/images/indicator.gif differ
diff --git a/src/main/webapp/images/success.png b/src/main/webapp/images/success.png
new file mode 100644 (file)
index 0000000..745c4da
Binary files /dev/null and b/src/main/webapp/images/success.png differ
diff --git a/src/main/webapp/sloController.jsp b/src/main/webapp/sloController.jsp
new file mode 100644 (file)
index 0000000..9575fd2
--- /dev/null
@@ -0,0 +1,178 @@
+<%@page import="edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext" %>
+<%@page import="edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContextStorageHelper" %>
+<%@page import="java.util.Locale" %>
+<%@page import="java.net.URLEncoder" %>
+<%@page import="java.io.UnsupportedEncodingException" %>
+<%
+SingleLogoutContext sloContext = SingleLogoutContextStorageHelper.getSingleLogoutContext(request);
+String contextPath = request.getContextPath();
+Locale defaultLocale = Locale.ENGLISH;
+Locale locale = request.getLocale();
+Boolean logoutString = false;
+Boolean sloFailed = false;
+Boolean sloAttempted = false;
+%>
+<html>
+    <head>
+        <link title="style" href="<%= contextPath %>/css/main.css" type="text/css" rel="stylesheet" />
+        <title>Shibboleth IdP Logout</title>
+        <script language="javascript" type="text/javascript">
+            <!--
+            var timer = 0;
+            var timeout;
+            
+            var xhr = new XMLHttpRequest();
+
+            function checkStatus() {
+                xhr.onreadystatechange = updateStatus;
+                xhr.open("GET", "<%= contextPath %>/SLOServlet?status", true);
+                xhr.send(null);
+            }
+
+            function updateStatus() {
+                if (xhr.readyState != 4 || xhr.status != 200) {
+                    return;
+                }
+
+                var sloFailed = false;
+                var resp = eval("(" + xhr.responseText + ")");
+                var ready = true;
+
+                for (var service in resp) {
+                    var entity = resp[service].entityID;
+                    var status = resp[service].logoutStatus;
+                    var src = "indicator.gif";
+                    
+                    switch(status) {
+                        case "LOGOUT_SUCCEEDED" : 
+                            src = "success.png";
+                            break;
+                        case "LOGOUT_FAILED" : 
+                        case "LOGOUT_UNSUPPORTED" :
+                        case "LOGOUT_TIMED_OUT" :
+                            src = "failed.png";
+                            sloFailed = true;
+                            break;
+                        case "LOGOUT_ATTEMPTED" : 
+                        case "LOGGED_IN" :
+                            if (timer >= 8) {
+                                src = "failed.png";
+                                sloFailed = true;
+                                ready = true;
+                            } else {
+                                src = "indicator.gif";
+                                ready = false;
+                            }
+                            break;
+                    }
+                    
+                    document.getElementById(entity).src = "<%= contextPath %>/images/" + src;
+                }
+
+                if (ready) {
+                    finish(sloFailed);
+                }
+            }
+
+            function finish(sloFailed) {
+                var str = "You have successfully logged out";
+                var className = "success";
+                if (sloFailed){
+                    str = "Logout failed. Please exit from your browser to complete the logout process." ;
+                    className = "fail";
+                }
+                document.getElementById("result").className = className;
+                document.getElementById("result").innerHTML = str;
+                document.getElementById("result").innerHTML += '<iframe src="<%= contextPath %>/SLOServlet?finish"></iframe><div class="clear"></div>';
+                clearTimeout(timeout);
+            }
+
+            function tick() {
+                timer += 1;
+                if (timer  == 1 || timer  == 2 || timer  == 4 || timer  == 8) {
+                    checkStatus();
+                }
+                if (timer > 8) {
+                    finish(true);
+                } else {
+                    timeout = setTimeout("tick()", 1000);
+                }
+            }
+
+            timeout = setTimeout("tick()", 1000);
+            //-->
+        </script>
+    </head>
+    <body>
+        <div class="content">
+            <h1>Logging out</h1>
+            <%
+            int i = 0;
+            for (SingleLogoutContext.LogoutInformation service : sloContext.getServiceInformation().values()) {
+                i++;
+                String entityID = null;
+                try {
+                    entityID = URLEncoder.encode(service.getEntityID(), "UTF-8");
+                } catch (UnsupportedEncodingException ex) {
+                    throw new RuntimeException(ex);
+                }
+                               
+                StringBuilder src = new StringBuilder(contextPath);
+                src.append("/images/");
+                switch (service.getLogoutStatus()) {
+                    case LOGGED_IN:
+                        logoutString = true;
+                    case LOGOUT_ATTEMPTED:
+                        sloAttempted = true;
+                        src.append("indicator.gif");
+                        break;
+                    case LOGOUT_UNSUPPORTED:
+                    case LOGOUT_FAILED:
+                    case LOGOUT_TIMED_OUT:
+                        sloFailed = true;
+                        src.append("failed.png");
+                        break;
+                    case LOGOUT_SUCCEEDED:
+                        logoutString = false;
+                        src.append("success.png");
+                        break;
+                }
+            %>
+            <div class="row">
+                <script type="text/javascript">
+                    <!--
+                    document.write('<%= service.getDisplayName(locale, defaultLocale) %><img alt="<%= service.getLogoutStatus().toString() %>" id="<%= service.getEntityID() %>" src="<%= src.toString() %>">');
+                    //-->
+                </script>
+                <noscript><%= service.getDisplayName(locale, defaultLocale) %> <% if (logoutString) { %><a href="<%= contextPath %>/SLOServlet?action&entityID=<%= entityID %>" target="_blank">Logout from this SP</a> <% }  else { %><img alt="<%= service.getLogoutStatus().toString() %>" id="<%= service.getEntityID() %>" src="<%= src.toString() %>"><% } %></noscript>
+            </div>
+            <%
+            if (service.isLoggedIn()) {
+                //if-logged-in
+            %>
+            <script type="text/javascript">
+                <!--
+                document.write('<iframe src="<%= contextPath %>/SLOServlet?action&entityID=<%= entityID %>" width="0" height="0"></iframe>');
+                //-->
+            </script>
+            <%
+            } //end of if-logged-in
+            } //end of for-each-service
+            %>
+            <div id="result"></div>
+            <noscript>
+                <p align="center">
+                    <% if (logoutString || sloAttempted) { %>
+                        <form action="<%= contextPath %>/SLOServlet" style="padding-top:10px;width:90%;clear:both;"><input type="hidden" name="logout" /><input type="submit" value="Refresh" /></form><div class="clear"></div>
+                    <% } else { %>
+                        <% if (sloFailed) { %>
+                            <div id="result" class="fail">Logout failed. Please exit from your browser to complete the logout process.</div>
+                        <% } else { %>
+                            <div id="result" class="success">You have successfully logged out<form action="<%= contextPath %>/SLOServlet" style="padding-top:10px;width:90%;clear:both;"><input type="hidden" name="finish" /><input type="submit" value="Finish logout" /></form><div class="clear"></div></div>
+                        <% }
+                       } %>
+                </p>
+            </noscript>
+        </div>
+    </body>
+</html>
diff --git a/src/main/webapp/sloQuestion.jsp b/src/main/webapp/sloQuestion.jsp
new file mode 100644 (file)
index 0000000..958e627
--- /dev/null
@@ -0,0 +1,49 @@
+<%@page import="edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContext" %>
+<%@page import="edu.internet2.middleware.shibboleth.idp.slo.SingleLogoutContextStorageHelper" %>
+<%@page import="java.util.Locale" %>
+<%
+SingleLogoutContext sloContext = SingleLogoutContextStorageHelper.getSingleLogoutContext(request);
+String contextPath = request.getContextPath();
+Locale defaultLocale = Locale.ENGLISH;
+Locale locale = request.getLocale();
+String requesterSP = sloContext.getServiceInformation().
+        get(sloContext.getRequesterEntityID()).getDisplayName(locale, defaultLocale);
+%>
+<html>
+    <head>
+        <link title="style" href="<%= contextPath %>/css/main.css" type="text/css" rel="stylesheet" />
+        <title>Shibboleth IdP Logout</title>
+    </head>
+    <body>
+        <div class="content">
+            <h1>Logging out</h1>
+            <h2>You have logged out from</h2>
+            <div class="row"><%= requesterSP %></div>
+                       <h2>You are logged in on these services</h2>
+            <%
+            int i = 0;
+            for (SingleLogoutContext.LogoutInformation service : sloContext.getServiceInformation().values()) {
+                if (service.getEntityID().equals(sloContext.getRequesterEntityID())) {
+                    continue;
+                }
+                i++;
+            %>
+            <div class="row"><%= service.getDisplayName(locale, defaultLocale) %></div>
+            <%
+            }
+            %>
+            <div class="controller">
+                Do you want to logout from all the services above?<br />
+                <form action="<%= contextPath %>/SLOServlet">
+                    <input type="hidden" name="logout"/>
+                    <input type="submit" value="Yes, all services" />
+                </form>
+                <form action="<%= contextPath %>/SLOServlet">
+                    <input type="hidden" name="finish"/>
+                    <input type="submit" value="No, only from <%= requesterSP %>" />
+                </form>
+                <div class="clear"></div>
+            </div>
+        </div>
+    </body>
+</html>
\ No newline at end of file