From: exe-cutor <exe-cutor>
Date: Thu, 4 Dec 2008 16:28:42 +0000 (+0000)
Subject: Merged Shibboleth logout support from 1.9 stable
X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=2db6ec19465e699373d883a5eac517e4c0e994aa;p=moodle.git

Merged Shibboleth logout support from 1.9 stable
---

diff --git a/auth/shibboleth/README.txt b/auth/shibboleth/README.txt
index d630413cbd..03b1d2bb9a 100644
--- a/auth/shibboleth/README.txt
+++ b/auth/shibboleth/README.txt
@@ -20,6 +20,7 @@ Changes:
 - 10. 2007: Removed the requirement for email address, surname and given name
             attributes on request of Markus Hagman
 - 11. 2007: Integrated WAYF Service in Moodle
+- 12. 2008: Single Logout support added
 
 Moodle Configuration with Dual login
 -------------------------------------------------------------------------------
@@ -87,7 +88,7 @@ Moodle Configuration with Dual login
                     moodle/auth/shibboleth/ is protected but *not* the other 
                     scripts and especially not the login.php script.
 
-5.  Save the changes for the 'Shibboleth settings'. T
+5.  Save the changes for the 'Shibboleth settings'.
 
     Important Note: If you went for 4.b (integrated WAYF service), saving the 
                     settings will overwrite the Moodle Alternate Login URL
@@ -199,6 +200,51 @@ Example file:
 ?>
 --
 
+
+How to add logout support
+--------------------------------------------------------------------------------
+
+In order make Moodle support Shibboleth logout, one has to make the Shibboleth 
+Service Provider (SP) aware of the Moodle logout capability. Only then the SP 
+can trigger Moodle's front or back channel logout handler.
+
+To make the SP aware of the Moodle logout, you have to add the following to the
+Shibboleth main configuration file shibboleth2.xml (usually in /etc/shibboleth/)
+just before the <MetadataProvider> element.
+
+--
+<Notify 
+	Channel="back"
+	Location="https://#YOUR_MOODLE_HOSTNAME#/moodle/auth/shibboleth/logout.php" />
+
+<Notify 
+	Channel="front"
+	Location="https://#YOUR_MOODLE_HOSTNAME#/moodle/auth/shibboleth/logout.php" />
+
+--
+
+The restart the Shibboleth daemon and check the log file for errors. If there 
+were no errors, you cat test the logout feature by accessing Moodle, 
+authenticating via Shibboleth and the access the URL:
+#YOUR_MOODLE_HOSTNAME#/Shibboleth.sso/Logout (assuming you have a standard 
+Shibboleth installation). If everything worked well, you should see a Shibboleth
+page saying that you were successfully logged out and if you go back to Moodle 
+you also should be logged out from Moodle.
+
+
+Limitations:
+Single Logout is only supported with SAML2 and so far only with the Shibboleth 
+Service Provider 2.x. 
+As of December 2008, the Shibboleth Identity Provider 2.1.1 does not yet support
+Single Logout (SLO). Therefore, the logout feature doesn't make that much 
+sense yet. One of the reasons why SLO isn't supported yet is because there aren't 
+ many applications yet that were adapted to support front and back channel 
+logout. Hopefully, the Moodle logout helps to motivate the developers to 
+implement SLO :)
+
+Also see https://spaces.internet2.edu/display/SHIB2/SLOIssues for some 
+background information.
+
 --------------------------------------------------------------------------------
 In case of problems and questions with Shibboleth authentication, contact
 Lukas Haemmerle <lukas.haemmerle@switch.ch> or Markus Hagman <hagman@hytti.uku.fi>
diff --git a/auth/shibboleth/auth.php b/auth/shibboleth/auth.php
index 8ed938150f..6fe37a5490 100644
--- a/auth/shibboleth/auth.php
+++ b/auth/shibboleth/auth.php
@@ -51,9 +51,27 @@ class auth_plugin_shibboleth extends auth_plugin_base {
      * @return bool Authentication success or failure.
      */
     function user_login($username, $password) {
-        
+       global $SESSION;
+
         // If we are in the shibboleth directory then we trust the server var
         if (!empty($_SERVER[$this->config->user_attribute])) {
+            // Associate Shibboleth session with user for SLO preparation
+            $sessionkey = '';
+            if (isset($_SERVER['Shib-Session-ID'])){
+                // This is only available for Shibboleth 2.x SPs
+                $sessionkey = $_SERVER['Shib-Session-ID'];
+            } else {
+                // Try to find out using the user's cookie
+                foreach ($_COOKIE as $name => $value){
+                    if (eregi('_shibsession_', $name)){
+                        $sessionkey = $value;
+                    }
+                }
+            }
+            
+            // Set shibboleth session ID for logout
+            $SESSION->shibboleth_session_id  = $sessionkey;
+
             return (strtolower($_SERVER[$this->config->user_attribute]) == strtolower($username));
         } else {
             // If we are not, the user has used the manual login and the login name is
diff --git a/auth/shibboleth/logout.php b/auth/shibboleth/logout.php
new file mode 100644
index 0000000000..3022ac7328
--- /dev/null
+++ b/auth/shibboleth/logout.php
@@ -0,0 +1,204 @@
+<?php // $Id$
+
+// Implements logout for Shibboleth authenticated users according to:
+// - https://spaces.internet2.edu/display/SHIB2/NativeSPLogoutInitiator
+// - https://spaces.internet2.edu/display/SHIB2/NativeSPNotify
+
+require_once("../../config.php");
+
+require_once($CFG->dirroot."/auth/shibboleth/auth.php");
+
+
+// Front channel logout
+if (
+		isset($_GET['return']) 
+		&& isset($_GET['action'])
+		&& $_GET['action'] == 'logout'
+   ){
+	
+	// Logout out user from application
+	// E.g. destroy application session/cookie etc
+	require_logout();
+	
+	// Finally, send user to the return URL
+	redirect($_GET['return']);
+}
+
+// Back channel logout
+elseif (!empty($HTTP_RAW_POST_DATA)) {
+	
+	// Requires PHP 5
+	
+	// Set SOAP header
+	$server = new SoapServer('https://'.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'/LogoutNotification.wsdl');
+	$server->addFunction("LogoutNotification");
+	$server->handle();
+} 
+
+// Return WSDL
+else {
+	
+	header('Content-Type: text/xml');
+	
+	echo <<<WSDL
+<?xml version ="1.0" encoding ="UTF-8" ?>
+<definitions name="LogoutNotification"
+  targetNamespace="urn:mace:shibboleth:2.0:sp:notify"
+  xmlns:notify="urn:mace:shibboleth:2.0:sp:notify"
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns="http://schemas.xmlsoap.org/wsdl/">
+
+<!--
+This page either has to be called with the GET arguments 'action' and 'return' via
+a redirect from the Shibboleth Service Provider logout handler (front-channel 
+logout) or via a SOAP request by a Shibboleth Service Provider (back-channel 
+logout).
+Because neither of these two variants seems to be the case, the WSDL file for 
+the web service is returned.
+
+For more information see:
+- https://spaces.internet2.edu/display/SHIB2/NativeSPLogoutInitiator
+- https://spaces.internet2.edu/display/SHIB2/NativeSPNotify
+-->
+
+	<types>
+	   <schema targetNamespace="urn:mace:shibboleth:2.0:sp:notify"
+		   xmlns="http://www.w3.org/2000/10/XMLSchema"
+		   xmlns:notify="urn:mace:shibboleth:2.0:sp:notify">
+			
+			<simpleType name="string">
+				<restriction base="string">
+					<minLength value="1"/>
+				</restriction>
+			</simpleType>
+			
+			<element name="OK" type="notify:OKType"/>
+			<complexType name="OKType">
+				<sequence/>
+			</complexType>
+			
+		</schema>
+	</types>
+	
+	<message name="getLogoutNotificationRequest">
+		<part name="SessionID" type="notify:string" />
+	</message>
+	
+	<message name="getLogoutNotificationResponse" >
+		<part name="OK"/>
+	</message>
+	
+	<portType name="LogoutNotificationPortType">
+		<operation name="LogoutNotification">
+			<input message="getLogoutNotificationRequest"/>
+			<output message="getLogoutNotificationResponse"/>
+		</operation>
+	</portType>
+	
+	<binding name="LogoutNotificationBinding" type="notify:LogoutNotificationPortType">
+		<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
+		<operation name="LogoutNotification">
+			<soap:operation soapAction="urn:xmethods-logout-notification#LogoutNotification"/>
+		</operation>
+	</binding>
+	
+	<service name="LogoutNotificationService">
+		  <port name="LogoutNotificationPort" binding="notify:LogoutNotificationBinding">
+			<soap:address location="https://{$_SERVER['HTTP_HOST']}{$_SERVER['PHP_SELF']}"/>
+		  </port>
+	</service>
+</definitions>
+WSDL;
+	exit;
+
+}
+
+/******************************************************************************/
+
+function LogoutNotification($SessionID){
+	
+	global $CFG, $SESSION;
+	
+	// Delete session of user using $SessionID
+	if(empty($CFG->dbsessions)) {
+		
+		// File session
+		$dir = $CFG->dataroot .'/sessions';
+		if (is_dir($dir)) {
+			if ($dh = opendir($dir)) {
+				while (($file = readdir($dh)) !== false) {
+					//echo $dir.'/'.$file."\n";exit;
+					if (is_file($dir.'/'.$file)){
+						$session_key = ereg_replace('sess_', '', $file);
+						
+						$data = file($dir.'/'.$file);
+					   if (isset($data[0])){
+							$user_session = unserializesession($data[0]);
+							
+							if (isset($user_session['SESSION']) && isset($user_session['SESSION']->shibboleth_session_id)){
+								//echo '2. Shibboleth Session (from filesystem session) of '.$user_session['USER']->username.':' .$user_session['SESSION']->shibboleth_session_id."\n";
+								// If there is a match, delete file
+								if ($user_session['SESSION']->shibboleth_session_id == $SessionID){
+									// Delete this file
+									if (!unlink($dir.'/'.$file)){
+										return new SoapFault('LogoutError', 'Could not delete Moodle session file.');
+									}
+								}
+							}
+							//print_r($user_session);
+						}
+						
+						//echo "Moodle session: $session_key \n";
+						//echo "filename: $file \n";
+					}
+				}
+				closedir($dh);
+			}
+		}
+	} else {
+		// DB Session
+		if (!empty($CFG->sessiontimeout)) {
+			$ADODB_SESS_LIFE   = $CFG->sessiontimeout;
+		}
+		
+			if ($user_session_data = get_records_sql('SELECT sesskey, sessdata FROM '. $CFG->prefix .'sessions2 WHERE expiry > NOW()')) {
+			foreach ($user_session_data as $session_data) {
+				
+				//print_r($session_data);
+				$user_session = adodb_unserialize( urldecode($session_data->sessdata) );
+				
+				if (isset($user_session['SESSION']) && isset($user_session['SESSION']->shibboleth_session_id)){
+					//echo '3. Shibboleth Session (from ADODB session) of '.$user_session['USER']->username.':' .$user_session['SESSION']->shibboleth_session_id."\n";
+					
+					// If there is a match, delete file
+					if ($user_session['SESSION']->shibboleth_session_id == $SessionID){
+						// Delete this session entry
+						if (ADODB_Session::destroy($session_data->sesskey) !== true){
+							return new SoapFault('LogoutError', 'Could not delete Moodle session entry in database.');
+						}
+					}
+				}
+				
+				//print_r($user_session);
+			}
+		}
+	}
+	
+	// If now SoapFault was thrown the function will return OK as the SP assumes
+	
+}
+
+/*****************************************************************************/
+
+// Same function as in adodb, but cannot be used for file session for some reason...
+function unserializesession( $serialized_string ){
+	$variables = array( );
+	$a = preg_split( "/(\w+)\|/", $serialized_string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
+	for( $i = 0; $i < count( $a ); $i = $i+2 ) {
+			$variables[$a[$i]] = unserialize( $a[$i+1] );
+	}
+	return( $variables );
+}
+
+
+?>