diff --git a/lib/SimpleSAML/Auth/ProcessingChain.php b/lib/SimpleSAML/Auth/ProcessingChain.php new file mode 100644 index 0000000000000000000000000000000000000000..4ba54ba4d4b0c4cc21879a60e4a904c362f2368c --- /dev/null +++ b/lib/SimpleSAML/Auth/ProcessingChain.php @@ -0,0 +1,218 @@ +<?php + +/** + * Class for implementing authentication processing chains for IdPs. + * + * This class implements a system for additional steps which should be taken by an IdP before + * submitting a response to a SP. Examples of additional steps can be additional authentication + * checks, or attribute consent requirements. + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ +class SimpleSAML_Auth_ProcessingChain { + + + /** + * The list of remaining filters which should be applied to the state. + */ + const FILTERS_INDEX = 'SimpleSAML_Auth_ProcessingChain.filters'; + + + /** + * The stage we use for completed requests. + */ + const COMPLETED_STAGE = 'SimpleSAML_Auth_ProcessingChain.completed'; + + + /** + * The request parameter we will use to pass the state identifier when we redirect after + * having completed processing of the state. + */ + const AUTHPARAM = 'AuthProcId'; + + + /** + * All authentication processing filters, in the order they should be applied. + */ + private $filters; + + + /** + * Initialize an authentication processing chain for the given service provider + * and identity provider. + * + * @param array $idpMetadata The metadata for the IdP. + * @param array $spMetadata The metadata for the SP. + */ + public function __construct($idpMetadata, $spMetadata) { + assert('is_array($idpMetadata)'); + assert('is_array($spMetadata)'); + + $this->filters = array(); + + if (array_key_exists('authproc', $idpMetadata)) { + $idpFilters = self::parseFilterList($idpMetadata['authproc']); + self::addFilters($this->filters, $idpFilters); + } + + if (array_key_exists('authproc', $spMetadata)) { + $spFilters = self::parseFilterList($spMetadata['authproc']); + self::addFilters($this->filters, $spFilters); + } + + + SimpleSAML_Logger::debug('Filter config for ' . $idpMetadata['entityid'] . '->' . + $spMetadata['entityid'] . ': ' . str_replace("\n", '', var_export($this->filters, TRUE))); + + } + + + /** + * Sort & merge filter configuration + * + * Inserts unsorted filters into sorted filter list. This sort operation is stable. + * + * @param array &$target Target filter list. This list must be sorted. + * @param array $src Source filters. May be unsorted. + */ + private static function addFilters(&$target, $src) { + assert('is_array($target)'); + assert('is_array($src)'); + + foreach ($src as $filter) { + $fp = $filter->priority; + + /* Find insertion position for filter. */ + for($i = count($target)-1; $i >= 0; $i--) { + if ($target[$i]->priority <= $fp) { + /* The new filter should be inserted after this one. */ + break; + } + } + /* $i now points to the filter which should preceede the current filter. */ + array_splice($target, $i+1, 0, array($filter)); + } + + } + + + /** + * Parse an array of authentication processing filters. + * + * @param array $filterSrc Array with filter configuration. + * @return array Array of SimpleSAML_Auth_ProcessingFilter objects. + */ + private static function parseFilterList($filterSrc) { + assert('is_array($filterSrc)'); + + $parsedFilters = array(); + + foreach ($filterSrc as $filter) { + + if (is_string($filter)) { + $filter = array($filter); + } + + if (!is_array($filter)) { + throw new Exception('Invalid authentication processing filter configuration: ' . + 'One of the filters wasn\'t a string or an array.'); + } + + $parsedFilters[] = self::parseFilter($filter); + } + + return $parsedFilters; + } + + + /** + * Parse an authentication processing filter. + * + * @param array $config Array with the authentication processing filter configuration. + * @return SimpleSAML_Auth_ProcessingFilter The parsed filter. + */ + private static function parseFilter($config) { + assert('is_array($config)'); + + if (!array_key_exists(0, $config)) { + throw new Exception('Authentication processing filter without name given.'); + } + + $className = SimpleSAML_Module::resolveClass($config[0], 'Auth_Process', + 'SimpleSAML_Auth_ProcessingFilter'); + + unset($config[0]); + return new $className($config, NULL); + } + + + /** + * Process the given state. + * + * This function will only return if processing completes. If processing requires showing + * a page to the user, we will redirect to the URL set in $state['ReturnURL'] after processing is + * completed. + * + * @param array &$state The state we are processing. + */ + public function processState(&$state) { + assert('is_array($state)'); + assert('array_key_exists("ReturnURL", $state)'); + + $state[self::FILTERS_INDEX] = $this->filters; + + while (count($state[self::FILTERS_INDEX]) > 0) { + $filter = array_shift($state[self::FILTERS_INDEX]); + $filter->process($state); + } + + /* Completed. */ + } + + + /** + * Continues processing of the state. + * + * This function is used to resume processing by filters which for example needed to show + * a page to the user. + * + * This function will never return. In the case of an exception, exception handling should + * be left to the main simpleSAMLphp exception handler. + * + * @param array $state The state we are processing. + */ + public static function resumeProcessing($state) { + assert('is_array($state)'); + + while (count($state[self::FILTERS_INDEX]) > 0) { + $filter = array_shift($state[self::FILTERS_INDEX]); + $filter->process($state); + } + + assert('array_key_exists("ReturnURL", $state)'); + + /* Completed. Save state information, and redirect to the URL specified + * in $state['ReturnURL']. + */ + $id = SimpleSAML_Auth_State::saveState($state, self::COMPLETED_STAGE); + SimpleSAML_Utilities::redirect($state['ReturnURL'], array(self::AUTHPARAM => $id)); + } + + + /** + * Retrieve a state which has finished processing. + * + * @param string $id The identifier of the state. This can be found in the request parameter + * with index from SimpleSAML_Auth_ProcessingChain::AUTHPARAM. + */ + public static function fetchProcessedState($id) { + assert('is_string($id)'); + + return SimpleSAML_Auth_State::loadState($id, self::COMPLETED_STAGE); + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/Auth/ProcessingFilter.php b/lib/SimpleSAML/Auth/ProcessingFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..a9d42aca4ac89b685dbcf52bb50413afcc6a2c6d --- /dev/null +++ b/lib/SimpleSAML/Auth/ProcessingFilter.php @@ -0,0 +1,69 @@ +<?php + + +/** + * Base class for authentication processing filters. + * + * All authentication processing filters must support serialization. + * + * The current request is stored in an associative array. It has the following defined attributes: + * - 'Attributes' The attributes of the user. + * - 'Destination' Metadata of the destination (SP). + * - 'Source' Metadata of the source (IdP). + * + * It may also contain other attributes. If an authentication processing filter wishes to store other + * information in it, it should have a name on the form 'module:filter:attributename', to avoid name + * collisions. + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ +abstract class SimpleSAML_Auth_ProcessingFilter { + + /** + * Priority of this filter. + * + * Used when merging IdP and SP processing chains. + * The priority can be any integer. The default for most filters is 50. Filters may however + * specify their own default, if they typically should be amongst the first or the last filters. + * + * The prioroty can also be overridden by the user by specifying the '%priority' option. + */ + public $priority = 50; + + + /** + * Constructor for a processing filter. + * + * Any processing filter which implements its own constructor must call this + * constructor first. + * + * @param array &$config Configuration for this filter. + * @param mixed $reserved For future use. + */ + public function __construct(&$config, $reserved) { + assert('is_array($config)'); + + if(array_key_exists('%priority', $config)) { + $this->priority = $config['%priority']; + if(!is_int($this->priority)) { + throw new Exception('Invalid priority: ' . var_export($this->priority, TRUE)); + } + unset($config['%priority']); + } + } + + + /** + * Process a request. + * + * When a filter returns from this function, it is assumed to have completed its task. + * + * @param array &$request The request we are currently processing. + */ + abstract public function process(&$request); + +} + +?> \ No newline at end of file diff --git a/modules/core/default-enable b/modules/core/default-enable new file mode 100644 index 0000000000000000000000000000000000000000..25615cb47c350d23033eb9801627ed8330bcc3e9 --- /dev/null +++ b/modules/core/default-enable @@ -0,0 +1,3 @@ +This file indicates that the default state of this module +is enabled. To disable, create a file named disable in the +same directory as this file. diff --git a/modules/core/lib/Auth/Process/AttributeAdd.php b/modules/core/lib/Auth/Process/AttributeAdd.php new file mode 100644 index 0000000000000000000000000000000000000000..5d31acae0ef07d92aa32f89fb53d19d600fe3f77 --- /dev/null +++ b/modules/core/lib/Auth/Process/AttributeAdd.php @@ -0,0 +1,113 @@ +<?php + +/** + * Filter to add attributes. + * + * This filter allows you to add attributes to the attribute set being processed. + * + * Example - add attribute, single value: + * <code> + * 'authproc' => array( + * array('core:AttributeAdd', 'source' => 'myidp'), + * ), + * </code> + * + * Examle - add attribute, multiple values: + * <code> + * 'authproc' => array( + * array('core:AttributeAdd', 'groups' => array('users', 'members')), + * ), + * </code> + * + * Examle - replace attribute, single value: + * <code> + * 'authproc' => array( + * array('core:AttributeAdd', '%replace', 'uid' => array('guest')), + * ), + * </code> + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_core_Auth_Process_AttributeAdd extends SimpleSAML_Auth_ProcessingFilter { + + /** + * Flag which indicates wheter this filter should append new values or replace old values. + */ + private $replace = FALSE; + + + /** + * Attributes which should be added/appended. + * + * Assiciative array of arrays. + */ + private $attributes = array(); + + + /** + * 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)'); + + foreach($config as $name => $values) { + if(is_int($name)) { + if($values === '%replace') { + $this->replace = TRUE; + } else { + throw new Exception('Unknown flag: ' . var_export($values, TRUE)); + } + continue; + } + + if(!is_string($name)) { + throw new Exception('Invalid attribute name: ' . var_export($name, TRUE)); + } + + if(!is_array($values)) { + $values = array($values); + } + foreach($values as $value) { + if(!is_string($value)) { + throw new Exception('Invalid value for attribute ' . $name . ': ' . + var_export($values, TRUE)); + } + } + + $this->attributes[$name] = $values; + } + } + + + /** + * Apply filter to add or replace attributes. + * + * Add or replace existing attributes with the configured values. + * + * @param array &$request The current request + */ + public function process(&$request) { + assert('is_array($request)'); + assert('array_key_exists("Attributes", $request)'); + + $attributes =& $request['Attributes']; + + foreach($this->attributes as $name => $values) { + if($this->replace === TRUE || !array_key_exists($name, $attributes)) { + $attributes[$name] = $values; + } else { + $attributes[$name] = array_merge($attributes[$name], $values); + } + } + } + +} + +?> \ No newline at end of file diff --git a/modules/core/lib/Auth/Process/AttributeLimit.php b/modules/core/lib/Auth/Process/AttributeLimit.php new file mode 100644 index 0000000000000000000000000000000000000000..d96df12c35d59aaed7fd5da8389b626019271a53 --- /dev/null +++ b/modules/core/lib/Auth/Process/AttributeLimit.php @@ -0,0 +1,70 @@ +<?php + +/** + * A filter for limiting which attributes are passed on. + * + * Example - remove all attributes except 'cn' and 'mail': + * <code> + * 'authproc' => array( + * array('core:AttributeLimit', 'cn', 'mail'), + * ), + * </code> + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_core_Auth_Process_AttributeLimit extends SimpleSAML_Auth_ProcessingFilter { + + /** + * List of attributes which this filter will allow through. + */ + private $allowedAttributes = array(); + + + /** + * 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)'); + + foreach($config as $name) { + + if(!is_string($name)) { + throw new Exception('Invalid attribute name: ' . var_export($name, TRUE)); + } + + $this->allowedAttributes[] = $name; + } + } + + + /** + * Apply filter to remove attributes. + * + * Removes all attributes which aren't one of the allowed attributes. + * + * @param array &$request The current request + */ + public function process(&$request) { + assert('is_array($request)'); + assert('array_key_exists("Attributes", $request)'); + + $attributes =& $request['Attributes']; + + foreach($attributes as $name => $values) { + if(!in_array($name, $this->allowedAttributes, TRUE)) { + unset($attributes[$name]); + } + } + + } + +} + +?> \ No newline at end of file diff --git a/modules/core/lib/Auth/Process/AttributeMap.php b/modules/core/lib/Auth/Process/AttributeMap.php new file mode 100644 index 0000000000000000000000000000000000000000..9631b0b9e90bcde38c74f1c608bfcb15c66abe85 --- /dev/null +++ b/modules/core/lib/Auth/Process/AttributeMap.php @@ -0,0 +1,108 @@ +<?php + +/** + * Attribute filter for renaming attributes. + * + * Example 1 - apply map stored in attributemap/defaultmap.php: + * <code> + * 'authproc' => array( + * array('core:AttributeMap', 'defaultmaps'), + * ), + * </code> + * + * Example 2 - rename attributes 'mail' and 'uid' to 'email' and 'user': + * <code> + * 'authproc' => array( + * array('core:AttributeMap', 'mail' => 'email', 'uid' => 'user'), + * ), + * </code> + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_core_Auth_Process_AttributeMap extends SimpleSAML_Auth_ProcessingFilter { + + /** + * Assosiative array with the mappings of attribute names. + */ + private $map = array(); + + + /** + * Initialize this filter, parse configuration + * + * @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)'); + + foreach($config as $origName => $newName) { + if(is_int($origName)) { + /* No index given - this is a map file. */ + $this->loadMapFile($newName); + continue; + } + + if(!is_string($origName)) { + throw new Exception('Invalid attribute name: ' . var_export($origName, TRUE)); + } + + if(!is_string($newName)) { + throw new Exception('Invalid attribute name: ' . var_export($newName, TRUE)); + } + + $this->map[$origName] = $newName; + } + } + + + /** + * Loads and merges in a file with a attribute map. + * + * @param string $fileName Name of attribute map file. Expected to be in the attributenamemapdir. + */ + private function loadMapFile($fileName) { + $config = SimpleSAML_Configuration::getInstance(); + $filePath = $config->getPathValue('attributenamemapdir') . $fileName . '.php'; + + if(!file_exists($filePath)) { + throw new Exception('Could not find attributemap file: ' . $filePath); + } + + $attributemap = NULL; + include($filePath); + if(!is_array($attributemap)) { + throw new Exception('Attribute map file "' . $filePath . '" didn\'t define an attribute map.'); + } + + $this->map = array_merge($this->map, $attributemap); + } + + + /** + * Apply filter to rename attributes. + * + * @param array &$request The current request + */ + public function process(&$request) { + assert('is_array($request'); + assert('array_key_exists("Attributes", $request)'); + + $attributes =& $request['Attributes']; + + foreach($attributes as $name => $values) { + if(array_key_exists($name, $this->map)) { + $attributes[$this->map[$name]] = $values; + unset($attributes[$name]); + } + } + + } + +} + +?> \ No newline at end of file diff --git a/modules/exampleauth/lib/Auth/Process/RedirectTest.php b/modules/exampleauth/lib/Auth/Process/RedirectTest.php new file mode 100644 index 0000000000000000000000000000000000000000..02a4220d6a3023c7dc9f32c2bb95f35cb547914d --- /dev/null +++ b/modules/exampleauth/lib/Auth/Process/RedirectTest.php @@ -0,0 +1,30 @@ +<?php + +/** + * A simple processing filter for testing that redirection works as it should. + * + */ +class sspmod_exampleauth_Auth_Process_RedirectTest extends SimpleSAML_Auth_ProcessingFilter { + + + /** + * Initialize processing of the redirect test. + * + * @param array &$state The state we should update. + */ + public function process(&$state) { + assert('is_array($state)'); + assert('array_key_exists("Attributes", $state)'); + + /* To check whether the state is saved correctly. */ + $state['Attributes']['RedirectTest1'] = array('OK'); + + /* Save state and redirect. */ + $id = SimpleSAML_Auth_State::saveState($state, 'exampleauth:redirectfilter-test'); + $url = SimpleSAML_Module::getModuleURL('exampleauth/redirecttest.php'); + SimpleSAML_Utilities::redirect($url, array('StateId' => $id)); + } + +} + +?> \ No newline at end of file diff --git a/modules/exampleauth/www/redirecttest.php b/modules/exampleauth/www/redirecttest.php new file mode 100644 index 0000000000000000000000000000000000000000..39a1b5450b381dbc7c5976c3be4e18f27894e342 --- /dev/null +++ b/modules/exampleauth/www/redirecttest.php @@ -0,0 +1,22 @@ +<?php + +/** + * Request handler for redirect filter test. + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ + +if (!array_key_exists('StateId', $_REQUEST)) { + throw new SimpleSAML_Error_BadRequest('Missing required StateId query parameter.'); +} + +$id = $_REQUEST['StateId']; +$state = SimpleSAML_Auth_State::loadState($id, 'exampleauth:redirectfilter-test'); + +$state['Attributes']['RedirectTest2'] = array('OK'); + +SimpleSAML_Auth_ProcessingChain::resumeProcessing($state); + +?> \ No newline at end of file diff --git a/www/saml2/idp/SSOService.php b/www/saml2/idp/SSOService.php index 55d53a06230acb697824946f10b0fa77ef2f3e74..b71d18711c8229ca793c9d74abbffeffa2d9433b 100644 --- a/www/saml2/idp/SSOService.php +++ b/www/saml2/idp/SSOService.php @@ -139,6 +139,13 @@ if (isset($_GET['SAMLRequest'])) { SimpleSAML_Utilities::fatalError($session->getTrackID(), 'CACHEAUTHNREQUEST', $exception); } +} elseif(isset($_REQUEST[SimpleSAML_Auth_ProcessingChain::AUTHPARAM])) { + + /* Resume from authentication processing chain. */ + $authProcId = $_REQUEST[SimpleSAML_Auth_ProcessingChain::AUTHPARAM]; + $authProcState = SimpleSAML_Auth_ProcessingChain::fetchProcessedState($authProcId); + $requestcache = $authProcState['core:saml20-idp:requestcache']; + } else { SimpleSAML_Utilities::fatalError($session->getTrackID(), 'SSOSERVICEPARAMS'); } @@ -270,6 +277,34 @@ if($needAuth && !$isPassive) { $filteredattributes = $afilter->getAttributes(); + + /* Authentication processing operations. */ + if (array_key_exists('AuthProcState', $requestcache)) { + /* Processed earlier, saved in requestcache. */ + $authProcState = $requestcache['AuthProcState']; + + } elseif (isset($authProcState)) { + /* Returned from redirect during processing. */ + $requestcache['AuthProcState'] = $authProcState; + + } else { + /* Not processed. */ + $pc = new SimpleSAML_Auth_ProcessingChain($idpmetadata, $spmetadata); + + $authProcState = array( + 'core:saml20-idp:requestcache' => $requestcache, + 'ReturnURL' => SimpleSAML_Utilities::selfURLNoQuery(), + 'Attributes' => $filteredattributes, + 'Destination' => $spmetadata, + 'Source' => $idpmetadata, + ); + + $pc->processState($authProcState); + + $requestcache['AuthProcState'] = $authProcState; + } + + $filteredattributes = $authProcState['Attributes']; diff --git a/www/shib13/idp/SSOService.php b/www/shib13/idp/SSOService.php index 495f20d48cc25f06106656906efbd82eea669279..0aecf9ca2b6ad6e9b228a090e9bfb9ed826077f4 100644 --- a/www/shib13/idp/SSOService.php +++ b/www/shib13/idp/SSOService.php @@ -91,6 +91,12 @@ if (isset($_GET['shire'])) { SimpleSAML_Utilities::fatalError($session->getTrackID(), 'CACHEAUTHNREQUEST', $exception); } +} elseif(isset($_REQUEST[SimpleSAML_Auth_ProcessingChain::AUTHPARAM])) { + + /* Resume from authentication processing chain. */ + $authProcId = $_REQUEST[SimpleSAML_Auth_ProcessingChain::AUTHPARAM]; + $authProcState = SimpleSAML_Auth_ProcessingChain::fetchProcessedState($authProcId); + $requestcache = $authProcState['core:shib13-idp:requestcache']; } else { SimpleSAML_Utilities::fatalError($session->getTrackID(), 'SSOSERVICEPARAMS'); @@ -184,6 +190,35 @@ if (!$session->isAuthenticated($authority) ) { $filteredattributes = $afilter->getAttributes(); + + /* Authentication processing operations. */ + if (array_key_exists('AuthProcState', $requestcache)) { + /* Processed earlier, saved in requestcache. */ + $authProcState = $requestcache['AuthProcState']; + + } elseif (isset($authProcState)) { + /* Returned from redirect during processing. */ + $requestcache['AuthProcState'] = $authProcState; + + } else { + /* Not processed. */ + $pc = new SimpleSAML_Auth_ProcessingChain($idpmetadata, $spmetadata); + + $authProcState = array( + 'core:shib13-idp:requestcache' => $requestcache, + 'ReturnURL' => SimpleSAML_Utilities::selfURLNoQuery(), + 'Attributes' => $filteredattributes, + 'Destination' => $spmetadata, + 'Source' => $idpmetadata, + ); + + $pc->processState($authProcState); + + $requestcache['AuthProcState'] = $authProcState; + } + + $filteredattributes = $authProcState['Attributes']; + /* * Dealing with attribute release consent.