From 27d9162a1fec531d4121eb78f8e99726229905b5 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Mon, 7 Mar 2011 13:26:39 +0000
Subject: [PATCH] cdc: New module for working with common domain cookies.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2761 44740490-163a-0410-bde0-09ae8108e29a
---
 modules/cdc/config-templates/module_cdc.php |  24 ++
 modules/cdc/default-disable                 |   3 +
 modules/cdc/lib/Auth/Process/CDC.php        |  72 ++++
 modules/cdc/lib/Client.php                  |  67 ++++
 modules/cdc/lib/Server.php                  | 404 ++++++++++++++++++++
 modules/cdc/www/resume.php                  |  22 ++
 modules/cdc/www/server.php                  |   3 +
 7 files changed, 595 insertions(+)
 create mode 100644 modules/cdc/config-templates/module_cdc.php
 create mode 100644 modules/cdc/default-disable
 create mode 100644 modules/cdc/lib/Auth/Process/CDC.php
 create mode 100644 modules/cdc/lib/Client.php
 create mode 100644 modules/cdc/lib/Server.php
 create mode 100644 modules/cdc/www/resume.php
 create mode 100644 modules/cdc/www/server.php

diff --git a/modules/cdc/config-templates/module_cdc.php b/modules/cdc/config-templates/module_cdc.php
new file mode 100644
index 000000000..77b83bdce
--- /dev/null
+++ b/modules/cdc/config-templates/module_cdc.php
@@ -0,0 +1,24 @@
+<?php
+
+$config = array(
+	'example.org' => array(
+
+		/*
+		 * The shared key for this CDC server.
+		 */
+		'key' => 'ExampleSharedKey',
+
+		/*
+		 * The URL to the server script.
+		 */
+		'server' => 'https://my-cdc.example.org/simplesaml/module.php/cdc/server.php',
+
+		/*
+		 * The lifetime of our cookie, in seconds.
+		 *
+		 * If this is 0, the cookie will expire when the browser is closed.
+		 */
+		'cookie.lifetime' => 0,
+
+	),
+);
diff --git a/modules/cdc/default-disable b/modules/cdc/default-disable
new file mode 100644
index 000000000..fa0bd82e2
--- /dev/null
+++ b/modules/cdc/default-disable
@@ -0,0 +1,3 @@
+This file indicates that the default state of this module
+is disabled. To enable, create a file named enable in the
+same directory as this file.
diff --git a/modules/cdc/lib/Auth/Process/CDC.php b/modules/cdc/lib/Auth/Process/CDC.php
new file mode 100644
index 000000000..16cf328f1
--- /dev/null
+++ b/modules/cdc/lib/Auth/Process/CDC.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * Filter for setting the SAML 2 common domain cookie.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_cdc_Auth_Process_CDC extends SimpleSAML_Auth_ProcessingFilter {
+
+
+	/**
+	 * Our CDC domain.
+	 *
+	 * @var string
+	 */
+	private $domain;
+
+
+	/**
+	 * Our CDC client.
+	 *
+	 * @var sspmod_cdc_Client
+	 */
+	private $client;
+
+
+	/**
+	 * Initialize this filter.
+	 *
+	 * @param array $config  Configuration information about this filter.
+	 * @param mixed $reserved  For future use.
+	 */
+	public function __construct($config, $reserved) {
+		parent::__construct($config, $reserved);
+		assert('is_array($config)');
+
+		if (!isset($config['domain'])) {
+			throw new SimpleSAML_Error_Exception('Missing domain option in cdc:CDC filter.');
+		}
+		$this->domain = (string)$config['domain'];
+
+		$this->client = new sspmod_cdc_Client($this->domain);
+	}
+
+
+	/**
+	 * Redirect to page setting CDC.
+	 *
+	 * @param array &$state  The request state.
+	 */
+	public function process(&$state) {
+		assert('is_array($state)');
+
+		if (!isset($state['Source']['entityid'])) {
+			SimpleSAML_Logger::warning('saml:CDC: Could not find IdP entityID.');
+			return;
+		}
+
+		/* Save state and build request. */
+		$id = SimpleSAML_Auth_State::saveState($state, 'cdc:resume');
+
+		$returnTo = SimpleSAML_Module::getModuleURL('cdc/resume.php', array('domain' => $this->domain));
+
+		$params = array(
+			'id' => $id,
+			'entityID' => $state['Source']['entityid'],
+		);
+		$this->client->sendRequest($returnTo, 'append', $params);
+	}
+
+}
diff --git a/modules/cdc/lib/Client.php b/modules/cdc/lib/Client.php
new file mode 100644
index 000000000..2e5043aa2
--- /dev/null
+++ b/modules/cdc/lib/Client.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * CDC client class.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_cdc_Client {
+
+	/**
+	 * Our CDC domain.
+	 *
+	 * @var string
+	 */
+	private $domain;
+
+
+	/**
+	 * The CDC server we send requests to.
+	 *
+	 * @var sspmod_cdc_Server|NULL
+	 */
+	private $server;
+
+
+	/**
+	 * Initialize a CDC client.
+	 *
+	 * @param string $domain  The domain we should query the server for.
+	 */
+	public function __construct($domain) {
+		assert('is_string($domain)');
+
+		$this->domain = $domain;
+		$this->server = new sspmod_cdc_Server($domain);
+	}
+
+
+	/**
+	 * Receive a CDC response.
+	 *
+	 * @return array|NULL  The response, or NULL if no response is received.
+	 */
+	public function getResponse() {
+
+		return $this->server->getResponse();
+	}
+
+
+	/**
+	 * Send a request.
+	 *
+	 * @param string $returnTo  The URL we should return to afterwards.
+	 * @param string $op  The operation we are performing.
+	 * @param array $params  Additional parameters.
+	 */
+	public function sendRequest($returnTo, $op, array $params = array()) {
+		assert('is_string($returnTo)');
+		assert('is_string($op)');
+
+		$params['op'] = $op;
+		$params['return'] = $returnTo;
+		$this->server->sendRequest($params);
+	}
+
+}
diff --git a/modules/cdc/lib/Server.php b/modules/cdc/lib/Server.php
new file mode 100644
index 000000000..dfc4f5a6e
--- /dev/null
+++ b/modules/cdc/lib/Server.php
@@ -0,0 +1,404 @@
+<?php
+
+/**
+ * CDC server class.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_cdc_Server {
+
+	/**
+	 * The domain.
+	 *
+	 * @var string
+	 */
+	private $domain;
+
+
+	/**
+	 * The URL to the server.
+	 *
+	 * @var string
+	 */
+	private $server;
+
+
+	/**
+	 * Our shared key.
+	 *
+	 * @var string
+	 */
+	private $key;
+
+
+	/**
+	 * The lifetime of our cookie, in seconds.
+	 *
+	 * If this is 0, the cookie will expire when the browser is closed.
+	 *
+	 * @param int
+	 */
+	private $cookieLifetime;
+
+
+	/**
+	 * Initialize a CDC server.
+	 *
+	 * @param string $domain  The domain we are a server for.
+	 */
+	public function __construct($domain) {
+		assert('is_string($domain)');
+
+		$cdcConfig = SimpleSAML_Configuration::getConfig('module_cdc.php');
+		$config = $cdcConfig->getConfigItem($domain, NULL);
+
+		if ($config === NULL) {
+			throw new SimpleSAML_Error_Exception('Unknown CDC domain: ' . var_export($domain, TRUE));
+		}
+
+		$this->domain = $domain;
+		$this->server = $config->getString('server');
+		$this->key = $config->getString('key');
+		$this->cookieLifetime = $config->getInteger('cookie.lifetime', 0);
+
+		if ($this->key === 'ExampleSharedKey') {
+			throw new SimpleSAML_Error_Exception('Key for CDC domain ' . var_export($domain, TRUE) . ' not changed from default.');
+		}
+	}
+
+
+	/**
+	 * Send a request to this CDC server.
+	 *
+	 * @param array $request  The CDC request.
+	 */
+	public function sendRequest(array $request) {
+		assert('isset($request["return"])');
+		assert('isset($request["op"])');
+
+		$request['domain'] = $this->domain;
+		$this->send($this->server, 'CDCRequest', $request);
+	}
+
+
+	/**
+	 * Parse and validate response received from a CDC server.
+	 *
+	 * @return array|NULL  The response, or NULL if no response is received.
+	 */
+	public function getResponse() {
+
+		$response = self::get('CDCResponse');
+		if ($response === NULL) {
+			return NULL;
+		}
+
+		if ($response['domain'] !== $this->domain) {
+			throw new SimpleSAML_Error_Exception('Response received from wrong domain.');
+		}
+
+		$this->validate('CDCResponse');
+
+		return $response;
+	}
+
+
+	/**
+	 * Parse and process a CDC request.
+	 */
+	public static function processRequest() {
+		$request = self::get('CDCRequest');
+		if ($request === NULL) {
+			throw new SimpleSAML_Error_BadRequest('Missing "CDCRequest" parameter.');
+		}
+
+		$domain = $request['domain'];
+		$server = new sspmod_cdc_Server($domain);
+
+		$server->handleRequest($request);
+	}
+
+
+	/**
+	 * Handle a parsed CDC requst.
+	 *
+	 * @param array $request
+	 */
+	private function handleRequest(array $request) {
+
+		if (!isset($request['op'])) {
+			throw new SimpleSAML_Error_BadRequest('Missing "op" in CDC request.');
+		}
+		$op = (string)$request['op'];
+
+		SimpleSAML_Logger::info('Received CDC request with "op": ' . var_export($op, TRUE));
+
+		if (!isset($request['return'])) {
+			throw new SimpleSAML_Error_BadRequest('Missing "return" in CDC request.');
+		}
+		$return = (string)$request['return'];
+
+		switch ($op) {
+		case 'append':
+			$response = $this->handleAppend($request);
+			break;
+		case 'delete':
+			$response = $this->handleDelete($request);
+			break;
+		case 'read':
+			$response = $this->handleRead($request);
+			break;
+		default:
+			$response = 'unknown-op';
+		}
+
+		if (is_string($response)) {
+			$response = array(
+				'status' => $response,
+			);
+		}
+
+		$response['op'] = $op;
+		if (isset($request['id'])) {
+			$response['id'] = (string)$request['id'];
+		}
+		$response['domain'] = $this->domain;
+
+		$this->send($return, 'CDCResponse', $response);
+	}
+
+
+	/**
+	 * Handle an append request.
+	 *
+	 * @param array $request  The request.
+	 * @return array  The response.
+	 */
+	private function handleAppend(array $request) {
+
+		if (!isset($request['entityID'])) {
+			throw new SimpleSAML_Error_BadRequest('Missing entityID in append request.');
+		}
+		$entityID = (string)$request['entityID'];
+
+		$list = $this->getCDC();
+
+		$prevIndex = array_search($entityID, $list, TRUE);
+		if ($prevIndex !== FALSE) {
+			unset($list[$prevIndex]);
+		}
+		$list[] = $entityID;
+
+		$this->setCDC($list);
+
+		return 'ok';
+	}
+
+
+	/**
+	 * Handle a delete request.
+	 *
+	 * @param array $request  The request.
+	 * @return array  The response.
+	 */
+	private function handleDelete(array $request) {
+
+		setcookie('_saml_idp', 'DELETE', time() - 86400 , '/', '.' . $this->domain, TRUE);
+		return 'ok';
+	}
+
+
+	/**
+	 * Handle a read request.
+	 *
+	 * @param array $request  The request.
+	 * @return array  The response.
+	 */
+	private function handleRead(array $request) {
+
+		$list = $this->getCDC();
+
+		return array(
+			'status' => 'ok',
+			'cdc' => $list,
+		);
+	}
+
+
+	/**
+	 * Helper function for parsing and validating a CDC message.
+	 *
+	 * @param string $parameter  The name of the query parameter.
+	 * @return array|NULL  The response, or NULL if no response is received.
+	 */
+	private static function get($parameter) {
+		assert('is_string($parameter)');
+
+		if (!isset($_REQUEST[$parameter])) {
+			return NULL;
+		}
+		$message = (string)$_REQUEST[$parameter];
+
+		$message = @base64_decode($message);
+		if ($message === FALSE) {
+			throw new SimpleSAML_Error_BadRequest('Error base64-decoding CDC message.');
+		}
+
+		$message = @json_decode($message, TRUE);
+		if ($message === FALSE) {
+			throw new SimpleSAML_Error_BadRequest('Error json-decoding CDC message.');
+		}
+
+		if (!isset($message['timestamp'])) {
+			throw new SimpleSAML_Error_BadRequest('Missing timestamp in CDC message.');
+		}
+		$timestamp = (int)$message['timestamp'];
+
+		if ($timestamp + 60 < time()) {
+			throw new SimpleSAML_Error_BadRequest('CDC signature has expired.');
+		}
+		if ($timestamp - 60 > time()) {
+			throw new SimpleSAML_Error_BadRequest('CDC signature from the future.');
+		}
+
+		if (!isset($message['domain'])) {
+			throw new SimpleSAML_Error_BadRequest('Missing domain in CDC message.');
+		}
+
+		return $message;
+	}
+
+
+	/**
+	 * Helper function for validating the signature on a CDC message.
+	 *
+	 * Will throw an exception if the message is invalid.
+	 *
+	 * @param string $parameter  The name of the query parameter.
+	 */
+	private function validate($parameter) {
+		assert('is_string($parameter)');
+		assert('isset($_REQUEST[$parameter])');
+
+		$message = (string)$_REQUEST[$parameter];
+
+		if (!isset($_REQUEST['Signature'])) {
+			throw new SimpleSAML_Error_BadRequest('Missing Signature on CDC message.');
+		}
+		$signature = (string)$_REQUEST['Signature'];
+
+		$cSignature = $this->calcSignature($message);
+		if ($signature !== $cSignature) {
+			throw new SimpleSAML_Error_BadRequest('Invalid signature on CDC message.');
+		}
+	}
+
+
+	/**
+	 * Helper function for sending CDC messages.
+	 *
+	 * @param string $to  The URL the message should be delivered to.
+	 * @param string $parameter  The query parameter the message should be sent in.
+	 * @param array $message  The CDC message.
+	 */
+	private function send($to, $parameter, array $message) {
+		assert('is_string($to)');
+		assert('is_string($parameter)');
+
+		$message['timestamp'] = time();
+		$message = json_encode($message);
+		$message = base64_encode($message);
+
+		$signature = $this->calcSignature($message);
+
+		$params = array(
+			$parameter => $message,
+			'Signature' => $signature,
+		);
+
+		$url = SimpleSAML_Utilities::addURLparameter($to, $params);
+		if (strlen($url) < 2048) {
+			SimpleSAML_Utilities::redirect($url);
+		} else {
+			SimpleSAML_Utilities::postRedirect($to, $params);
+		}
+	}
+
+
+	/**
+	 * Calculate the signature on the given message.
+	 *
+	 * @param string $rawMessage  The base64-encoded message.
+	 * @return string  The signature.
+	 */
+	private function calcSignature($rawMessage) {
+		assert('is_string($rawMessage)');
+
+		return sha1($this->key . $rawMessage . $this->key);
+	}
+
+
+	/**
+	 * Get the IdP entities saved in the common domain cookie.
+	 *
+	 * @return array  List of IdP entities.
+	 */
+	private function getCDC() {
+
+		if (!isset($_COOKIE['_saml_idp'])) {
+			return array();
+		}
+
+		$ret = (string)$_COOKIE['_saml_idp'];
+		$ret = explode(' ', $ret);
+		foreach ($ret as &$idp) {
+			$idp = base64_decode($idp);
+			if ($idp === FALSE) {
+				/* Not properly base64 encoded. */
+				SimpleSAML_Logger::warning('CDC - Invalid base64-encoding of CDC entry.');
+				return array();
+			}
+		}
+
+		return $ret;
+	}
+
+
+	/**
+	 * Build a CDC cookie string.
+	 *
+	 * @param array $list  The list of IdPs.
+	 * @return string  The CDC cookie value.
+	 */
+	function setCDC(array $list) {
+
+		foreach ($list as &$value) {
+			$value = base64_encode($value);
+		}
+
+		$cookie = implode(' ', $list);
+
+		while (strlen($cookie) > 4000) {
+			/* The cookie is too long. Remove the oldest elements until it is short enough. */
+			$tmp = explode(' ', $cookie, 2);
+			if (count($tmp) === 1) {
+				/*
+				 * We are left with a single entityID whose base64
+				 * representation is too long to fit in a cookie.
+				 */
+				break;
+			}
+			$cookie = $tmp[1];
+		}
+
+		if ($this->cookieLifetime === 0) {
+			$expire = 0;
+		} else {
+			$expire = time() + $this->cookieLifetime;
+		}
+
+		setcookie('_saml_idp', $cookie, $expire, '/', '.' . $this->domain, TRUE);
+	}
+
+}
diff --git a/modules/cdc/www/resume.php b/modules/cdc/www/resume.php
new file mode 100644
index 000000000..6e4fcf381
--- /dev/null
+++ b/modules/cdc/www/resume.php
@@ -0,0 +1,22 @@
+<?php
+
+
+if (!array_key_exists('domain', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing domain to CDC resume handler.');
+}
+
+$domain = (string)$_REQUEST['domain'];
+$client = new sspmod_cdc_Client($domain);
+
+$response = $client->getResponse();
+if ($response === NULL) {
+	throw new SimpleSAML_Error_BadRequest('Missing CDC response to CDC resume handler.');
+}
+
+if (!isset($response['id'])) {
+	throw new SimpleSAML_Error_BadRequest('CDCResponse without id.');
+}
+
+$state = SimpleSAML_Auth_State::loadState($response['id'], 'cdc:resume');
+
+SimpleSAML_Auth_ProcessingChain::resumeProcessing($state);
diff --git a/modules/cdc/www/server.php b/modules/cdc/www/server.php
new file mode 100644
index 000000000..f84b7a906
--- /dev/null
+++ b/modules/cdc/www/server.php
@@ -0,0 +1,3 @@
+<?php
+
+sspmod_cdc_Server::processRequest();
\ No newline at end of file
-- 
GitLab