diff --git a/.travis.yml b/.travis.yml index 4f83a53..fe03a6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ php: - 5.5 - 5.6 - 7.0 +- 7.4 +- 8.1 - hhvm matrix: allow_failures: diff --git a/composer.json b/composer.json index 291a57a..63ef876 100644 --- a/composer.json +++ b/composer.json @@ -1,16 +1,23 @@ { "name": "niif/simplesamlphp-module-attributeaggregator", - "description": "Attribute Aggregator implementation or SAML AttributeQuery", + "description": "Attribute Aggregator implementation for SAML AttributeQuery", "type": "simplesamlphp-module", "require": { "simplesamlphp/composer-module-installer": "~1.1", "ext-soap": "*" }, "require-dev": { - "simplesamlphp/simplesamlphp": ">=1.14", - "phpunit/phpunit": "~3.7" + "simplesamlphp/simplesamlphp": "^2.0", + "phpunit/phpunit": "^9.5" + }, + "autoload": { + "psr-4": { + "SimpleSAML\\Module\\AttributeAggregator\\": "src/" + } }, "autoload-dev": { - "files": ["tests/_autoload_modules.php"] + "files": [ + "tests/_autoload_modules.php" + ] } } diff --git a/public/attributequery.php b/public/attributequery.php new file mode 100644 index 0000000..675acdb --- /dev/null +++ b/public/attributequery.php @@ -0,0 +1,227 @@ +getArray('metadata.sources', []); + +foreach ($metadataSources as $source) { + try { + $mdq = MetaDataStorageSource::getSource($source); + $aaMetadata = $mdq->getMetaData($state['attributeaggregator:entityId'],'attributeauthority-remote'); + + if ($aaMetadata) { + if (array_keys($aaMetadata) === range(0, count($aaMetadata) - 1)) { + $aaMetadata = $aaMetadata[0]; + } + break; + } + } catch (Exception $e) { + Logger::warning('Metadata lookup failed:' . $e->getMessage()); + } +} + +if (!$aaMetadata) { + throw new Exception( + 'attributeaggregator: AA entityId (' . $state['attributeaggregator:entityId'] . + ') does not exist in any available metadata sources.' + ); +} + + +/* Find an AttributeService with SOAP binding */ +$aas = $aaMetadata['AttributeService']; + +if (!is_array($aas)) { + throw new Exception("AttributeService is missing or invalid in metadata for entityId: " . var_export($aaMetadata, true)); +} + +for ($i=0;$igenerateID(); +$session->setData('attributeaggregator:data', $dataId, $data, 3600); + +$nameId = new NameID(); +$nameId->setFormat($data['nameIdFormat']); +$nameId->setValue($data['nameIdValue']); +$nameId->setNameQualifier($data['nameIdQualifier']); +$nameId->setSPNameQualifier($data['nameIdSPQualifier']); + +if (empty($nameId->getNameQualifier())) { + $nameId->setNameQualifier(NULL); +} +if (empty($nameId->getSPNameQualifier())) { + $nameId->setSPNameQualifier(NULL); +} + + + +$attributes = $state['attributeaggregator:attributes']; +$attributes_to_send = array(); +foreach ($attributes as $name => $params) { + if (array_key_exists('values', $params)){ + $attributes_to_send[$name] = $params['values']; + } + else { + $attributes_to_send[$name] = array(); + } +} + +$attributeNameFormat = $state['attributeaggregator:attributeNameFormat']; + +$authsource = Source::getById($state["attributeaggregator:authsourceId"]); +$src = $authsource->getMetadata(); +$dst = Configuration::loadFromArray($aaMetadata, 'attributeauthority-remote' . '/' . var_export($state['attributeaggregator:entityId'], true)); + +// Sending query +try { + $response = sendQuery($dataId, $data['url'], $nameId, $attributes_to_send, $attributeNameFormat, $src, $dst); +} catch (Exception $e) { + throw new Exception('[attributeaggregator] Got an exception while performing attribute query. Exception: '.get_class($e).', message: '.$e->getMessage()); +} + +$idpEntityId = $response->getIssuer(); +if ($idpEntityId === NULL) { + throw new Exception('Missing issuer in response.'); +} +$assertions = Message::processResponse($src, $dst, $response); +$attributes_from_aa = $assertions[0]->getAttributes(); +$expected_attributes = $state['attributeaggregator:attributes']; +// get attributes from response, and put it in the state. +foreach ($attributes_from_aa as $name=>$values){ + // expected? + if (array_key_exists($name, $expected_attributes)){ + // There is in the existing attributes? + if(array_key_exists($name, $state['Attributes'])){ + // has multiSource rule? + if (! empty($expected_attributes[$name]['multiSource'])){ + switch ($expected_attributes[$name]['multiSource']) { + case 'override': + $state['Attributes'][$name] = $values; + break; + case 'keep': + continue 2; + break; + case 'merge': + $state['Attributes'][$name] = array_merge($state['Attributes'][$name], $values); + break; + } + } + // default: merge the attributes + else { + $state['Attributes'][$name] = array_merge($state['Attributes'][$name], $values); + } + } + // There is not in the existing attributes, create it. + else { + $state['Attributes'][$name] = $values; + } + } + // not expected? Put it to attributes array. + else { + if (!empty($state['Attributes'][$name])){ + $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); + } + else + $state['Attributes'][$name] = $values; + } +} + +Logger::debug('[attributeaggregator] - Attributes now:'.var_export($state['Attributes'],true)); +ProcessingChain::resumeProcessing($state); +exit; + +/** + * build and send AttributeQuery + */ +function sendQuery($dataId, $url, $nameId, $attributes, $attributeNameFormat,$src,$dst) { + Assert::string($dataId); + Assert::string($url); + Assert::isInstanceOf($nameId, NameID::class); + Assert::isArray($attributes); + + Logger::debug('[attributeaggregator] - sending request'); + + $issuer = new \SAML2\XML\saml\Issuer(); + $issuer->setValue($src->getValue('entityid')); + + $query = new AttributeQuery(); + $query->setRelayState($dataId); + $query->setDestination($url); + $query->setIssuer($issuer); + $query->setNameId($nameId); + $query->setAttributeNameFormat($attributeNameFormat); + + if (! empty($attributes)){ + $query->setAttributes($attributes); + } + + Message::addSign($src,$dst,$query); + + if (! $query->getSignatureKey()){ + throw new Exception('[attributeaggregator] - Unable to find private key for signing attribute request.'); + } + + Logger::debug('[attributeaggregator] - sending attribute query: '.var_export($query, true)); + $binding = new SOAPClient(); + + $result = $binding->send($query, $src, $dst); + return $result; +} \ No newline at end of file diff --git a/lib/Auth/Process/attributeaggregator.php b/src/Auth/Process/AttributeAggregator.php similarity index 51% rename from lib/Auth/Process/attributeaggregator.php rename to src/Auth/Process/AttributeAggregator.php index 97949fa..3ec90ed 100644 --- a/lib/Auth/Process/attributeaggregator.php +++ b/src/Auth/Process/AttributeAggregator.php @@ -1,4 +1,7 @@ getArray('metadata.sources', []); + + foreach ($metadataSources as $source) { + try { + $mdq = MetaDataStorageSource::getSource($source); + $aameta = $mdq->getMetaData($config['entityId'], 'attributeauthority-remote'); - if ($config['entityId']) { - $aameta = $metadata->getMetaData($config['entityId'], 'attributeauthority-remote'); - if (!$aameta) { - throw new SimpleSAML_Error_Exception( - 'attributeaggregator: AA entityId (' . $config['entityId'] . - ') does not exist in the attributeauthority-remote metadata set.' - ); + if ($aameta) { + break; + } + } catch (Exception $e) { + Logger::warning('Metadata lookup failed:' . $e->getMessage()); } - $this->entityId = $config['entityId']; } - else { - throw new SimpleSAML_Error_Exception( - 'attributeaggregator: AA entityId is not specified in the configuration.' - ); + + if (!$aameta) { + throw new Exception( + 'attributeaggregator: AA entityId (' . $config['entityId'] . + ') does not exist in any available metadata sources.' + ); } + $this->entityId = $config['entityId']; + if (! empty($config["attributeId"])){ $this->attributeId = $config["attributeId"]; } - + if (! empty($config["required"])){ $this->required = $config["required"]; } if (!empty($config["nameIdFormat"])){ - foreach (array( - SAML2_Const::NAMEID_UNSPECIFIED, - SAML2_Const::NAMEID_PERSISTENT, - SAML2_Const::NAMEID_TRANSIENT, - SAML2_Const::NAMEID_ENCRYPTED) as $format) { + foreach ([ Constants::NAMEID_UNSPECIFIED, + Constants::NAMEID_PERSISTENT, + Constants::NAMEID_TRANSIENT, + Constants::NAMEID_ENCRYPTED] as $format) { $invalid = TRUE; if ($config["nameIdFormat"] == $format) { $this->nameIdFormat = $config["nameIdFormat"]; @@ -105,25 +137,25 @@ public function __construct($config, $reserved) } } if ($invalid) - throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid nameIdFormat: ".$config["nameIdFormat"]); + throw new Exception("attributeaggregator: Invalid nameIdFormat: ".$config["nameIdFormat"]); } if (!empty($config["attributes"])){ if (! is_array($config["attributes"])) { - throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid format of attributes array in the configuration"); + throw new Exception("attributeaggregator: Invalid format of attributes array in the configuration"); } foreach ($config["attributes"] as $attribute) { if (! is_array($attribute)) { - throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid format of attributes array in the configuration"); + throw new Exception("attributeaggregator: Invalid format of attributes array in the configuration"); } if (array_key_exists("values", $attribute)) { if (! is_array($attribute["values"])) { - throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid format of attributes array in the configuration"); - } + throw new Exception("attributeaggregator: Invalid format of attributes array in the configuration"); + } } if (array_key_exists('multiSource', $attribute)){ if(! preg_match('/^(merge|keep|override)$/', $attribute['multiSource'])) - throw new SimpleSAML_Error_Exception( + throw new Exception( 'attributeaggregator: Invalid multiSource value '.$attribute['multiSource'].' for '.key($attribute).'. It not mached keep, merge or override.' ); } @@ -132,10 +164,9 @@ public function __construct($config, $reserved) } if (!empty($config["attributeNameFormat"])){ - foreach (array( - SAML2_Const::NAMEFORMAT_UNSPECIFIED, - SAML2_Const::NAMEFORMAT_URI, - SAML2_Const::NAMEFORMAT_BASIC) as $format) { + foreach ([ Constants::NAMEFORMAT_UNSPECIFIED, + Constants::NAMEFORMAT_URI, + Constants::NAMEFORMAT_BASIC] as $format) { $invalid = TRUE; if ($config["attributeNameFormat"] == $format) { $this->attributeNameFormat = $config["attributeNameFormat"]; @@ -144,7 +175,7 @@ public function __construct($config, $reserved) } } if ($invalid) - throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid attributeNameFormat: ".$config["attributeNameFormat"], 1); + throw new Exception("attributeaggregator: Invalid attributeNameFormat: ".$config["attributeNameFormat"], 1); } } @@ -158,9 +189,9 @@ public function __construct($config, $reserved) * * @return void */ - public function process(&$state) + public function process(array &$state): void { - assert('is_array($state)'); + Assert::isArray($state); $state['attributeaggregator:authsourceId'] = $state["saml:sp:State"]["saml:sp:AuthId"]; $state['attributeaggregator:entityId'] = $this->entityId; @@ -172,16 +203,18 @@ public function process(&$state) if (! $state['attributeaggregator:attributeId']){ if (! $this->required) { - SimpleSAML_Logger::info('[attributeaggregator] This user session does not have '.$this->attributeId.', which is required for querying the AA! Continue processing.'); - SimpleSAML_Logger::debug('[attributeaggregator] Attributes are: '.var_export($state['Attributes'],true)); + Logger::info('[attributeaggregator] This user session does not have '.$this->attributeId.', which is required for querying the AA! Continue processing.'); + Logger::debug('[attributeaggregator] Attributes are: '.var_export($state['Attributes'],true)); SimpleSAML_Auth_ProcessingChain::resumeProcessing($state); - } - throw new SimpleSAML_Error_Exception("This user session does not have ".$this->attributeId.", which is required for querying the AA! Attributes are: ".var_export($state['Attributes'],1)); + } + throw new Exception("This user session does not have ".$this->attributeId.", which is required for querying the AA! Attributes are: ".var_export($state['Attributes'],1)); } - - // Save state and redirect - $id = SimpleSAML_Auth_State::saveState($state, 'attributeaggregator:request'); - $url = SimpleSAML_Module::getModuleURL('attributeaggregator/attributequery.php'); - SimpleSAML_Utilities::redirect($url, array('StateId' => $id)); // FIXME: redirect is deprecated + + $id = State::saveState($state, 'attributeaggregator:request'); + $url = Module::getModuleURL('attributeaggregator/attributequery.php'); + $params = ['StateId' => $id]; + + $httpUtils = new HTTP(); + $httpUtils->redirectTrustedURL($url, $params); } } diff --git a/tests/_autoload_modules.php b/tests/_autoload_modules.php index a6fdea7..4011497 100644 --- a/tests/_autoload_modules.php +++ b/tests/_autoload_modules.php @@ -1,32 +1,48 @@ , UNINETT + * @package SimpleSAMLphp */ /** - * Autoload function for local SimpleSAMLphp modules. + * Autoload function for SimpleSAMLphp modules following PSR-4. * * @param string $className Name of the class. */ -function SimpleSAML_test_module_autoload($className) +function sspmodAutoloadPSR4(string $className): void { - $modulePrefixLength = strlen('sspmod_'); - $classPrefix = substr($className, 0, $modulePrefixLength); - if ($classPrefix !== 'sspmod_') { - return; + $elements = explode('\\', $className); + if ($elements[0] === '') { + // class name starting with /, ignore + array_shift($elements); + } + if (count($elements) < 4) { + return; // it can't be a module + } + if (array_shift($elements) !== 'SimpleSAML') { + return; // the first element is not "SimpleSAML" + } + if (array_shift($elements) !== 'Module') { + return; // the second element is not "module" } - $modNameEnd = strpos($className, '_', $modulePrefixLength); - $moduleClass = substr($className, $modNameEnd + 1); + // this is a SimpleSAMLphp module following PSR-4 + $module = array_shift($elements); + if (!\SimpleSAML\Module::isModuleEnabled($module)) { + return; // module not enabled, avoid giving out any information at all + } - $file = dirname(dirname(__FILE__)) . '/lib/' . str_replace('_', '/', $moduleClass) . '.php'; + $file = \SimpleSAML\Module::getModuleDir($module) . '/src/' . implode('/', $elements) . '.php'; if (file_exists($file)) { require_once($file); } } -spl_autoload_register('SimpleSAML_test_module_autoload'); +spl_autoload_register('sspmodAutoloadPSR4'); \ No newline at end of file diff --git a/tests/lib/Auth/Process/attributeaggregatorTest.php b/tests/lib/Auth/Process/attributeaggregatorTest.php deleted file mode 100644 index af3fe11..0000000 --- a/tests/lib/Auth/Process/attributeaggregatorTest.php +++ /dev/null @@ -1,23 +0,0 @@ -process($request); - return $request; - } - - public function testAny() - { - $this->assertTrue(true, 'Just for travis.yml test'); - } -} diff --git a/tests/src/Auth/Process/AttributeAggregatorTest.php b/tests/src/Auth/Process/AttributeAggregatorTest.php new file mode 100644 index 0000000..9ef9516 --- /dev/null +++ b/tests/src/Auth/Process/AttributeAggregatorTest.php @@ -0,0 +1,30 @@ +process($request); + return $request; + } + + public function testAny(): void + { + $this->assertTrue(true, 'Just for travis.yml test'); + } +} diff --git a/www/attributequery.php b/www/attributequery.php deleted file mode 100644 index 55ebaa0..0000000 --- a/www/attributequery.php +++ /dev/null @@ -1,166 +0,0 @@ -getMetadata($state['attributeaggregator:entityId'],'attributeauthority-remote'); - -/* Find an AttributeService with SOAP binding */ -$aas = $aaMetadata['AttributeService']; -for ($i=0;$isetData('attributeaggregator:data', $dataId, $data, 3600); - -$nameId = array( - 'Format' => $data['nameIdFormat'], - 'Value' => $data['nameIdValue'], - 'NameQualifier' => $data['nameIdQualifier'], - 'SPNameQualifier' => $data['nameIdSPQualifier'], -); -if (empty($nameId['NameQualifier'])) { - $nameId['NameQualifier'] = NULL; -} -if (empty($nameId['SPNameQualifier'])) { - $nameId['SPNameQualifier'] = NULL; -} - -$attributes = $state['attributeaggregator:attributes']; -$attributes_to_send = array(); -foreach ($attributes as $name => $params) { - if (array_key_exists('values', $params)){ - $attributes_to_send[$name] = $params['values']; - } - else { - $attributes_to_send[$name] = array(); - } -} - -$attributeNameFormat = $state['attributeaggregator:attributeNameFormat']; - -$authsource = SimpleSAML_Auth_Source::getById($state["attributeaggregator:authsourceId"]); -$src = $authsource->getMetadata(); -$dst = $metadata->getMetaDataConfig($state['attributeaggregator:entityId'],'attributeauthority-remote'); - -// Sending query -try { - $response = sendQuery($dataId, $data['url'], $nameId, $attributes_to_send, $attributeNameFormat, $src, $dst); -} catch (Exception $e) { - throw new SimpleSAML_Error_Exception('[attributeaggregator] Got an exception while performing attribute query. Exception: '.get_class($e).', message: '.$e->getMessage()); -} - -$idpEntityId = $response->getIssuer(); -if ($idpEntityId === NULL) { - throw new SimpleSAML_Error_Exception('Missing issuer in response.'); -} -$assertions = sspmod_saml_Message::processResponse($src, $dst, $response); -$attributes_from_aa = $assertions[0]->getAttributes(); -$expected_attributes = $state['attributeaggregator:attributes']; -// get attributes from response, and put it in the state. -foreach ($attributes_from_aa as $name=>$values){ - // expected? - if (array_key_exists($name, $expected_attributes)){ - // There is in the existing attributes? - if(array_key_exists($name, $state['Attributes'])){ - // has multiSource rule? - if (! empty($expected_attributes[$name]['multiSource'])){ - switch ($expected_attributes[$name]['multiSource']) { - case 'override': - $state['Attributes'][$name] = $values; - break; - case 'keep': - continue; - break; - case 'merge': - $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); - break; - } - } - // default: merge the attributes - else { - $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); - } - } - // There is not in the existing attributes, create it. - else { - $state['Attributes'][$name] = $values; - } - } - // not expected? Put it to attributes array. - else { - if (!empty($state['Attributes'][$name])){ - $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); - } - else - $state['Attributes'][$name] = $values; - } -} - -SimpleSAML_Logger::debug('[attributeaggregator] - Attributes now:'.var_export($state['Attributes'],true)); -SimpleSAML_Auth_ProcessingChain::resumeProcessing($state); -exit; - -/** - * build and send AttributeQuery - */ -function sendQuery($dataId, $url, $nameId, $attributes, $attributeNameFormat,$src,$dst) { - assert('is_string($dataId)'); - assert('is_string($url)'); - assert('is_array($nameId)'); - assert('is_array($attributes)'); - - SimpleSAML_Logger::debug('[attributeaggregator] - sending request'); - - $query = new SAML2_AttributeQuery(); - $query->setRelayState($dataId); - $query->setDestination($url); - $query->setIssuer($src->getValue('entityid')); - $query->setNameId($nameId); - $query->setAttributeNameFormat($attributeNameFormat); - if (! empty($attributes)){ - $query->setAttributes($attributes); - } - sspmod_saml_Message::addSign($src,$dst,$query); - - if (! $query->getSignatureKey()){ - throw new SimpleSAML_Error_Exception('[attributeaggregator] - Unable to find private key for signing attribute request.'); - } - - SimpleSAML_Logger::debug('[attributeaggregator] - sending attribute query: '.var_export($query,1)); - $binding = new SAML2_SOAPClient(); - - $result = $binding->send($query, $src, $dst); - return $result; -}