diff --git a/modules/aselect/docs/aselect.txt b/modules/aselect/docs/aselect.txt index 9e3f91cb1c70b56ade5840c7737923695470a911..756798e12fc3a23eb183a483b7e9f1eba29d816d 100644 --- a/modules/aselect/docs/aselect.txt +++ b/modules/aselect/docs/aselect.txt @@ -1,64 +1,44 @@ -Using the A-Select authentication source with simpleSAMLphp -=========================================================== +A-Select module for simpleSAMLphp +--------------------------------- -This authentication source for A-Select is based on the a-select -handler by Hans Zandbelt. The original source combines the possibility -to use an A-Select server as an authentication source and the possibility -to use simpleSAMLphp as an A-Select server. This module only acts as a -authentication source. Signing is not (yet) supported. +This module allows one to use an A-Select server as authentication +source for simpleSAMLphp. -The structure applied follows the structure of the CAS authentication source. +The module supports the A-Select protocol, including signing of +requests. Not supported is A-Select Cross. +Usage: -Setting up the A-Select authentication module ----------------------------------------------- +Enable the module if not already enabled: +$ touch modules/aselect/enabled -The first thing you need to do is to enable the aselect module: +In config/authsources.php, configure your A-Selectserver as an +authentication source. The following is an example for a source +named 'aselect': - touch modules/aselect/enable - -The A-Select authentication module has two modes of operation. - -1. The module can act to the A-Select server as an application. - -Configuration in A-Select: - - <application id="app1" level="30"> - <attribute_policy>policyA</attribute_policy> - <forced_authenticate>false</forced_authenticate> - </application> - -Configuration in authsources.php - - 'example-aselect' => array( + 'aselect' => array( 'aselect:aselect', - 'serverurl' => 'http://a-select.dev.han.nl:8080/aselectserver/server', - 'serverid' => 'hanaselect', - 'type' => 'app', # type = app/cross - 'app_id' => 'app1', # only if type = app + 'app_id' => 'simplesamlphp', + 'server_id' => 'sso.example.com', + 'server_url' => 'https://test.sso.example.com/server', + 'private_key' => 'file:///etc/ssl/private/aselect.key' ), -2. The module can act to the A-Select server as cross A-Select. +The parameters: +- app_id: the application I for simpleSAMLphp as configured in + your A-Select server; +- server_id: the A-Select server ID as configured in your + A-Select server; +- server_url: the URL for your A-Selectserver, usually ends in + '/server/. +- private_key: the key you want to use for signing requests. + If you're really sure you do not want request signing, you + can set this option to a null value. +Options 'serverurl' and 'serverid' (without underscore) are +supported for backwards compatibility. -Configuration in A-Select: - - <cross_aselect> - <local_servers require_signing="false"> - <organization id="simpleSAMLphp" server="sso.testorg.com" attribute_policy="policyA"> - </organization> - </local_servers> - </cross_aselect> - - -Configuration in authsources.php - - 'example-aselect' => array( - 'aselect:aselect', - 'serverurl' => 'http://a-select.dev.han.nl:8080/aselectserver/server', - 'serverid' => 'hanaselect', - 'type' => 'cross', # type = app/cross - 'local_organization' => 'simpleSAMLphp', # only if type = cross - 'required_level' => 10, # only if type = cross, defaults to 10 - ), +Author: Wessel Dankers <wsl@uvt.nl> +Copyright: © 2011,2012 Tilburg University (http://www.tilburguniversity.edu) +License: GPL version 3 or any later version. diff --git a/modules/aselect/lib/Auth/Source/aselect.php b/modules/aselect/lib/Auth/Source/aselect.php index 2cba46470614d2277d8ccc65bb4536a501cade3d..c7cb88d2f789b7a370a6c8924c760dc4ee8bf531 100644 --- a/modules/aselect/lib/Auth/Source/aselect.php +++ b/modules/aselect/lib/Auth/Source/aselect.php @@ -1,30 +1,15 @@ <?php /** - * A-Select authentication source. + * Authentication module which acts as an A-Select client * - * Based on www/aselect/handler.php by Hans Zandbelt, SURFnet BV. <hans.zandbelt@surfnet.nl> - * - * @author Patrick Honing, Hogeschool van Arnhem en Nijmegen. <Patrick.Honing@han.nl> - * @package simpleSAMLphp - * @version $Id$ + * @author Wessel Dankers, Tilburg University */ class sspmod_aselect_Auth_Source_aselect extends SimpleSAML_Auth_Source { - - /** - * The string used to identify our states. - */ - const STAGE_INIT = 'aselect:init'; - - /** - * The key of the AuthId field in the state. - */ - const AUTHID = 'aselect:AuthId'; - - /** - * @var array with aselect configuration - */ - private $asconfig; + private $app_id = 'simplesamlphp'; + private $server_id; + private $server_url; + private $private_key; /** * Constructor for this authentication source. @@ -33,142 +18,186 @@ class sspmod_aselect_Auth_Source_aselect extends SimpleSAML_Auth_Source { * @param array $config Configuration. */ public function __construct($info, $config) { - assert('is_array($info)'); - assert('is_array($config)'); - /* Call the parent constructor first, as required by the interface. */ parent::__construct($info, $config); - if (!array_key_exists('serverurl', $config)) throw new Exception('aselect serverurl not specified'); - $this->asconfig['serverurl'] = $config['serverurl']; + $cfg = SimpleSAML_Configuration::loadFromArray($config, + 'Authentication source ' . var_export($this->authId, true)); - if (!array_key_exists('serverid', $config)) throw new Exception('aselect serverid not specified'); - $this->asconfig['serverid'] = $config['serverid']; + $cfg->getValueValidate('type', array('app'), 'app'); + $this->app_id = $cfg->getString('app_id'); + $this->private_key = $cfg->getString('private_key', null); - if (!array_key_exists('type', $config)) throw new Exception('aselect type not specified'); - $this->asconfig['type'] = $config['type']; + // accept these arguments with '_' for consistency + // accept these arguments without '_' for backwards compatibility + $this->server_id = $cfg->getString('serverid', null); + if($this->server_id === null) + $this->server_id = $cfg->getString('server_id'); - if ($this->asconfig['type'] == 'app') { - if (!array_key_exists('app_id', $config)) throw new Exception('aselect app_id not specified'); - $this->asconfig['app_id'] = $config['app_id']; - } elseif($this->asconfig['type'] == 'cross') { - if (!array_key_exists('local_organization', $config)) throw new Exception('aselect local_organization not specified'); - $this->asconfig['local_organization'] = $config['local_organization']; + $this->server_url = $cfg->getString('serverurl', null); + if($this->server_url === null) + $this->server_url = $cfg->getString('server_url'); + } - $this->asconfig['required_level'] = (array_key_exists('required_level', $config)) ? $config['required_level'] : 10; - } else { - throw new Exception('aselect type need to be either app or cross'); - } + /** + * Initiate authentication. + * + * @param array &$state Information about the current authentication. + */ + public function authenticate(&$state) { + $state['aselect::authid'] = $this->authId; + $id = SimpleSAML_Auth_State::saveState($state, 'aselect:login', true); + + try { + $app_url = SimpleSAML_Module::getModuleURL('aselect/credentials.php', array('ssp_state' => $id)); + $as_url = $this->request_authentication($app_url); + SimpleSAML_Utilities::redirect($as_url); + } catch(Exception $e) { + // attach the exception to the state + SimpleSAML_Auth_State::throwException($state, $e); + } } + /** + * Sign a string using the configured private key + * + * @param string $str The string to calculate a signature for + */ + private function base64_signature($str) { + $key = openssl_pkey_get_private($this->private_key); + if($key === false) + throw new SimpleSAML_Error_Exception("Unable to load private key: ".openssl_error_string()); + if(!openssl_sign($str, $sig, $key)) + throw new SimpleSAML_Error_Exception("Unable to create signature: ".openssl_error_string()); + openssl_pkey_free($key); + return base64_encode($sig); + } - // helper function for sending a non-browser request to a remote server - function as_call($url) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_URL, $url); - $result = curl_exec($ch); - $error = curl_error($ch); - curl_close($ch); - if ($result == FALSE) { - throw new Exception('Request on remote server failed: ' . $error); - } - $parms = array(); - foreach (explode('&', $result) as $parm) { - $tuple = explode('=', $parm); - $parms[urldecode($tuple[0])] = urldecode($tuple[1]); - } - if ($parms['result_code'] != '0000') { - throw new Exception('Request on remote server returned error: ' . $result); + /** + * Parse a base64 encoded attribute blob. Can't use parse_str() because it + * may contain multi-valued attributes. + * + * @param string $base64 The base64 string to decode. + */ + private static function decode_attributes($base64) { + $blob = base64_decode($base64, true); + if($blob === false) + throw new SimpleSAML_Error_Exception("Attributes parameter base64 malformed"); + $pairs = explode('&', $blob); + $ret = array(); + foreach($pairs as $pair) { + $keyval = explode('=', $pair, 2); + if(count($keyval) < 2) + throw new SimpleSAML_Error_Exception("Missing value in attributes parameter"); + $key = urldecode($keyval[0]); + $val = urldecode($keyval[1]); + $ret[$key][] = $val; } - return $parms; + return $ret; } + /** + * Default options for curl invocations. + */ + private static $curl_options = array( + CURLOPT_BINARYTRANSFER => true, + CURLOPT_FAILONERROR => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 1, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => "simpleSAMLphp", + ); /** - * Log-in using A-Select + * Create a (possibly signed) URL to contact the A-Select server. * - * @param array &$state Information about the current authentication. + * @param string $request The name of the request (authenticate / verify_credentials). + * @param array $parameters The parameters to pass for this request. */ - public function authenticate(&$state) { - assert('is_array($state)'); - - /* We are going to need the authId in order to retrieve this authentication source later. */ - $state[self::AUTHID] = $this->authId; - - $stateID = SimpleSAML_Auth_State::saveState($state, self::STAGE_INIT); - - $serviceUrl = SimpleSAML_Module::getModuleURL('aselect/linkback.php', array('stateID' => $stateID)); - - if ($this->asconfig['type'] == 'app') { - $params = array( - 'request' => 'authenticate', - 'a-select-server' => $this->asconfig['serverid'], - 'app_id' => $this->asconfig['app_id'], - 'app_url' => $serviceUrl, - ); - } else { // type = cross - $params = array( - 'request' => 'authenticate', - 'a-select-server' => $this->asconfig['serverid'], - 'local_organization' => $this->asconfig['local_organization'], - 'required_level' => $this->asconfig['required_level'], - 'local_as_url' => $serviceUrl, - - ); + private function create_aselect_url($request, $parameters) { + $parameters['request'] = $request; + $parameters['a-select-server'] = $this->server_id; + if(!is_null($this->private_key)) { + $signable = ''; + foreach(array('a-select-server', 'app_id', 'app_url', 'aselect_credentials', 'rid') as $p) + if(array_key_exists($p, $parameters)) + $signable .= $parameters[$p]; + $parameters['signature'] = $this->base64_signature($signable); } - $url = SimpleSAML_Utilities::addURLparameter($this->asconfig['serverurl'],$params); + return SimpleSAML_Utilities::addURLparameter($this->server_url, $parameters); + } + + /** + * Contact the A-Select server and return the result as an associative array. + * + * @param string $request The name of the request (authenticate / verify_credentials). + * @param array $parameters The parameters to pass for this request. + */ + private function call_aselect($request, $parameters) { + $url = $this->create_aselect_url($request, $parameters); + + $curl = curl_init($url); + if($curl === false) + throw new SimpleSAML_Error_Exception("Unable to create CURL handle"); + + if(!curl_setopt_array($curl, self::$curl_options)) + throw new SimpleSAML_Error_Exception("Unable to set CURL options: ".curl_error($curl)); + + $str = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if($str === false) + throw new SimpleSAML_Error_Exception("Unable to retrieve URL: $error"); - $parm = $this->as_call($url); + parse_str($str, $res); - SimpleSAML_Utilities::redirect( - $parm['as_url'], - array( - 'rid' => $parm['rid'], - 'a-select-server' => $this->asconfig['serverid'], - ) - ); + // message is only available with some A-Select server implementations + if($res['result_code'] != '0000') + if(array_key_exists('message', $res)) + throw new SimpleSAML_Error_Exception("Unable to contact SSO service: result_code=".$res['result_code']." message=".$res['message']); + else + throw new SimpleSAML_Error_Exception("Unable to contact SSO service: result_code=".$res['result_code']); + unset($res['result_code']); + + return $res; } - public function finalStep(&$state) { - $credentials = $state['aselect:credentials']; - $rid = $state['aselect:rid']; - assert('isset($credentials)'); - assert('isset($rid)'); - - $params = array( - 'request' => 'verify_credentials', - 'rid' => $rid, - 'a-select-server' => $this->asconfig['serverid'], - 'aselect_credentials' => $credentials, - ); - if ($this->asconfig['type'] == 'cross') { - $params['local_organization'] = $this->asconfig['local_organization']; - } + /** + * Initiate authentication. Returns a URL to redirect the user to. + * + * @param string $app_url The SSP URL to return to after authenticating (similar to an ACS). + */ + public function request_authentication($app_url) { + $res = $this->call_aselect('authenticate', + array('app_id' => $this->app_id, 'app_url' => $app_url)); - $url = SimpleSAML_Utilities::addURLparameter($this->asconfig['serverurl'], $params); - - $parms = $this->as_call($url); - $attributes = array('uid' => array($parms['uid'])); - - if (array_key_exists('attributes', $parms)) { - $decoded = base64_decode($parms['attributes']); - foreach (explode('&', $decoded) as $parm) { - $tuple = explode('=', $parm); - $name = urldecode($tuple[0]); - if (preg_match('/\[\]$/',$name)) { - $name = substr($name, 0 ,-2); - } - if (!array_key_exists($name, $attributes)) { - $attributes[$name] = array(); - } - $attributes[$name][] = urldecode($tuple[1]); - } - } - $state['Attributes'] = $attributes; + $as_url = $res['as_url']; + unset($res['as_url']); + + return SimpleSAML_Utilities::addURLparameter($as_url, $res); + } + + /** + * Verify the credentials upon return from the A-Select server. Returns an associative array + * with the information given by the A-Select server. Any attributes are pre-parsed. + * + * @param string $server_id The A-Select server ID as passed by the client + * @param string $credentials The credentials as passed by the client + * @param string $rid The request ID as passed by the client + */ + public function verify_credentials($server_id, $credentials, $rid) { + if($server_id != $this->server_id) + throw new SimpleSAML_Error_Exception("Acquired server ID ($server_id) does not match configured server ID ($this->server_id)"); + + $res = $this->call_aselect('verify_credentials', + array('aselect_credentials' => $credentials, 'rid' => $rid)); + + if(array_key_exists('attributes', $res)) + $res['attributes'] = self::decode_attributes($res['attributes']); - SimpleSAML_Auth_Source::completeAuth($state); + return $res; } } diff --git a/modules/aselect/www/credentials.php b/modules/aselect/www/credentials.php new file mode 100644 index 0000000000000000000000000000000000000000..3d3b8cba1204a903c768285f831c544bc253a113 --- /dev/null +++ b/modules/aselect/www/credentials.php @@ -0,0 +1,47 @@ +<?php + +/** + * Check the credentials that the user got from the A-Select server. + * This function is called after the user returns from the A-Select server. + * + * @author Wessel Dankers, Tilburg University + */ +function check_credentials() { + $state = SimpleSAML_Auth_State::loadState($_REQUEST['ssp_state'], 'aselect:login'); + + if(!array_key_exists('a-select-server', $_REQUEST)) + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Missing a-select-server parameter")); + $server_id = $_REQUEST['a-select-server']; + + if(!array_key_exists('aselect_credentials', $_REQUEST)) + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Missing aselect_credentials parameter")); + $credentials = $_REQUEST['aselect_credentials']; + + if(!array_key_exists('rid', $_REQUEST)) + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Missing rid parameter")); + $rid = $_REQUEST['rid']; + + try { + if(!array_key_exists('aselect::authid', $state)) + throw new SimpleSAML_Error_Exception("ASelect authentication source missing in state"); + $authid = $state['aselect::authid']; + $aselect = SimpleSAML_Auth_Source::getById($authid); + if(is_null($aselect)) + throw new SimpleSAML_Error_Exception("Could not find authentication source with id $authid"); + $creds = $aselect->verify_credentials($server_id, $credentials, $rid); + + if(array_key_exists('attributes', $creds)) { + $state['Attributes'] = $creds['attributes']; + } else { + $res = $creds['res']; + $state['Attributes'] = array('uid' => array($res['uid']), 'organization' => array($res['organization'])); + } + } catch(Exception $e) { + SimpleSAML_Auth_State::throwException($state, $e); + } + + SimpleSAML_Auth_Source::completeAuth($state); + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Internal error in A-Select component")); +} + +check_credentials(); diff --git a/modules/aselect/www/linkback.php b/modules/aselect/www/linkback.php index 1f3efba228777d941f009f22ce31942e48689ea2..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/modules/aselect/www/linkback.php +++ b/modules/aselect/www/linkback.php @@ -1,36 +0,0 @@ -<?php - -/** - * Handle linkback() response from A-Select. - */ - - -if (!isset($_GET['stateID'])) { - throw new SimpleSAML_Error_BadRequest('Missing stateID parameter.'); -} -$stateId = (string)$_GET['stateID']; - -if (!isset($_GET['aselect_credentials'])) { - throw new SimpleSAML_Error_BadRequest('Missing aselect_credentials parameter.'); -} -if (!isset($_GET['rid'])) { - throw new SimpleSAML_Error_BadRequest('Missing ridparameter.'); -} - - -$state = SimpleSAML_Auth_State::loadState($stateId, sspmod_aselect_Auth_Source_aselect::STAGE_INIT); -$state['aselect:credentials'] = $_GET['aselect_credentials']; -$state['aselect:rid'] = $_GET['rid']; - - -/* Find authentication source. */ -assert('array_key_exists(sspmod_aselect_Auth_Source_aselect::AUTHID, $state)'); -$sourceId = $state[sspmod_aselect_Auth_Source_aselect::AUTHID]; - -$source = SimpleSAML_Auth_Source::getById($sourceId); -if ($source === NULL) { - throw new Exception('Could not find authentication source with id ' . $sourceId); -} - -$source->finalStep($state); -