diff --git a/modules/cdc/config-templates/module_cdc.php b/modules/cdc/config-templates/module_cdc.php new file mode 100644 index 0000000000000000000000000000000000000000..77b83bdceceb63f8319440f810a8175e2d3daa7e --- /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 0000000000000000000000000000000000000000..fa0bd82e2df7bd79d57593d35bc53c1f9d3ef71f --- /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 0000000000000000000000000000000000000000..16cf328f16b20ca68dff5d9c91ee046c2dadf318 --- /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 0000000000000000000000000000000000000000..2e5043aa241ffdee83bbfc4822a034d20694ea3d --- /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 0000000000000000000000000000000000000000..dfc4f5a6efefb85b2df4cf6c17036e077a0c37eb --- /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 0000000000000000000000000000000000000000..6e4fcf381caa8613dbda4f593bf06c29481ea039 --- /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 0000000000000000000000000000000000000000..f84b7a90614528eecf3b4f97930d8ceadc8011a1 --- /dev/null +++ b/modules/cdc/www/server.php @@ -0,0 +1,3 @@ +<?php + +sspmod_cdc_Server::processRequest(); \ No newline at end of file