From 4da039070f5e20453819c8b723a18e13ade85baf Mon Sep 17 00:00:00 2001 From: Czier Norbert Date: Mon, 7 Apr 2025 15:03:25 +0200 Subject: [PATCH 1/4] Updated to SSP 2.x --- composer.json | 15 ++- {www => public}/attributequery.php | 112 +++++++++++------- .../Auth/Process/AttributeAggregator.php | 87 ++++++++------ tests/_autoload_modules.php | 40 +++++-- .../Auth/Process/attributeaggregatorTest.php | 23 ---- .../Auth/Process/AttributeAggregatorTest.php | 30 +++++ 6 files changed, 190 insertions(+), 117 deletions(-) rename {www => public}/attributequery.php (55%) rename lib/Auth/Process/attributeaggregator.php => src/Auth/Process/AttributeAggregator.php (62%) delete mode 100644 tests/lib/Auth/Process/attributeaggregatorTest.php create mode 100644 tests/src/Auth/Process/AttributeAggregatorTest.php 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/www/attributequery.php b/public/attributequery.php similarity index 55% rename from www/attributequery.php rename to public/attributequery.php index 55ebaa0..0517a1a 100644 --- a/www/attributequery.php +++ b/public/attributequery.php @@ -1,30 +1,50 @@ getMetadata($state['attributeaggregator:entityId'],'attributeauthority-remote'); /* Find an AttributeService with SOAP binding */ $aas = $aaMetadata['AttributeService']; for ($i=0;$igenerateID(); $session->setData('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; +$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['SPNameQualifier'])) { - $nameId['SPNameQualifier'] = NULL; +if (empty($nameId->getSPNameQualifier())) { + $nameId->setSPNameQualifier(NULL); } + + $attributes = $state['attributeaggregator:attributes']; $attributes_to_send = array(); foreach ($attributes as $name => $params) { @@ -70,22 +93,22 @@ $attributeNameFormat = $state['attributeaggregator:attributeNameFormat']; -$authsource = SimpleSAML_Auth_Source::getById($state["attributeaggregator:authsourceId"]); +$authsource = 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); + $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()); + 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 SimpleSAML_Error_Exception('Missing issuer in response.'); + throw new Exception('Missing issuer in response.'); } -$assertions = sspmod_saml_Message::processResponse($src, $dst, $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. @@ -101,16 +124,16 @@ $state['Attributes'][$name] = $values; break; case 'keep': - continue; + continue 2; break; case 'merge': - $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); - break; + $state['Attributes'][$name] = array_merge($state['Attributes'][$name], $values); + break; } } // default: merge the attributes else { - $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); + $state['Attributes'][$name] = array_merge($state['Attributes'][$name], $values); } } // There is not in the existing attributes, create it. @@ -128,39 +151,44 @@ } } -SimpleSAML_Logger::debug('[attributeaggregator] - Attributes now:'.var_export($state['Attributes'],true)); -SimpleSAML_Auth_ProcessingChain::resumeProcessing($state); +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('is_string($dataId)'); - assert('is_string($url)'); - assert('is_array($nameId)'); - assert('is_array($attributes)'); + Assert::string($dataId); + Assert::string($url); + Assert::isInstanceOf($nameId, NameID::class); + Assert::isArray($attributes); + + Logger::debug('[attributeaggregator] - sending request'); - SimpleSAML_Logger::debug('[attributeaggregator] - sending request'); + $issuer = new \SAML2\XML\saml\Issuer(); + $issuer->setValue($src->getValue('entityid')); - $query = new SAML2_AttributeQuery(); + $query = new AttributeQuery(); $query->setRelayState($dataId); $query->setDestination($url); - $query->setIssuer($src->getValue('entityid')); + $query->setIssuer($issuer); $query->setNameId($nameId); $query->setAttributeNameFormat($attributeNameFormat); + if (! empty($attributes)){ $query->setAttributes($attributes); } - sspmod_saml_Message::addSign($src,$dst,$query); + + Message::addSign($src,$dst,$query); if (! $query->getSignatureKey()){ - throw new SimpleSAML_Error_Exception('[attributeaggregator] - Unable to find private key for signing attribute request.'); + throw new 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(); + + 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 62% rename from lib/Auth/Process/attributeaggregator.php rename to src/Auth/Process/AttributeAggregator.php index 97949fa..dcc50d1 100644 --- a/lib/Auth/Process/attributeaggregator.php +++ b/src/Auth/Process/AttributeAggregator.php @@ -1,4 +1,7 @@ getMetaData($config['entityId'], 'attributeauthority-remote'); if (!$aameta) { - throw new SimpleSAML_Error_Exception( + throw new Exception( 'attributeaggregator: AA entityId (' . $config['entityId'] . ') does not exist in the attributeauthority-remote metadata set.' ); @@ -78,7 +94,7 @@ public function __construct($config, $reserved) $this->entityId = $config['entityId']; } else { - throw new SimpleSAML_Error_Exception( + throw new Exception( 'attributeaggregator: AA entityId is not specified in the configuration.' ); } @@ -86,17 +102,16 @@ public function __construct($config, $reserved) 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 +120,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 +147,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 +158,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,7 +172,7 @@ public function __construct($config, $reserved) * * @return void */ - public function process(&$state) + public function process(array &$state): void { assert('is_array($state)'); $state['attributeaggregator:authsourceId'] = $state["saml:sp:State"]["saml:sp:AuthId"]; @@ -172,16 +186,17 @@ 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 + + $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'); + } +} From 57f982f55a5014edd3dbb29ed79fa8d245e373b0 Mon Sep 17 00:00:00 2001 From: Czier Norbert Date: Tue, 8 Apr 2025 14:25:26 +0200 Subject: [PATCH 2/4] Updated to use "mdx/mdq" metadata sources as well --- .travis.yml | 2 + public/attributequery.php | 39 +++++++++++++++++-- src/Auth/Process/AttributeAggregator.php | 48 ++++++++++++++++-------- 3 files changed, 71 insertions(+), 18 deletions(-) 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/public/attributequery.php b/public/attributequery.php index 0517a1a..683c6f6 100644 --- a/public/attributequery.php +++ b/public/attributequery.php @@ -11,6 +11,7 @@ use SimpleSAML\Error\Exception; use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; +use SimpleSAML\Metadata\MetaDataStorageSource; use SimpleSAML\Module\saml\Message; use SimpleSAML\Session; use SimpleSAML\Utils\Random; @@ -21,7 +22,6 @@ $session = Session::getSessionFromRequest(); -$metadata = MetaDataStorageHandler::getMetadataHandler(); if (!array_key_exists('StateId', $_REQUEST)) { throw new BadRequest( @@ -32,10 +32,43 @@ $id = $_REQUEST['StateId']; $state = State::loadState($id, 'attributeaggregator:request'); Logger::info('[attributeaggregator] - Querying attributes from ' . $state['attributeaggregator:entityId'] ); -$aaMetadata = $metadata->getMetadata($state['attributeaggregator:entityId'],'attributeauthority-remote'); + +$aaMetadata = null; + +$globalConfig = Configuration::getInstance(); +$metadataSources = $globalConfig->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;$igetMetadata(); -$dst = $metadata->getMetaDataConfig($state['attributeaggregator:entityId'],'attributeauthority-remote'); +$dst = Configuration::loadFromArray($aaMetadata, 'attributeauthority-remote' . '/' . var_export($state['attributeaggregator:entityId'], true)); // Sending query try { diff --git a/src/Auth/Process/AttributeAggregator.php b/src/Auth/Process/AttributeAggregator.php index dcc50d1..79065eb 100644 --- a/src/Auth/Process/AttributeAggregator.php +++ b/src/Auth/Process/AttributeAggregator.php @@ -20,8 +20,10 @@ use SimpleSAML\Error\Exception; use SimpleSAML\Logger; use SimpleSAML\Module; +use SimpleSAML\Configuration; use SimpleSAML\Utils\HTTP; -use SimpleSAML\Metadata\MetaDataStorageHandler; +use SimpleSAML\Metadata\MetaDataStorageSource; +use SimpleSAML\Metadata\Sources\MDQ; use SAML2\Constants; class AttributeAggregator extends ProcessingFilter @@ -78,27 +80,42 @@ class AttributeAggregator extends ProcessingFilter */ public function __construct(array $config, $reserved) { - assert('is_array($config)'); + Assert::isArray($config); parent::__construct($config, $reserved); - $metadata = MetaDataStorageHandler::getMetadataHandler(); + if (empty($config['entityId'])) { + throw new Exception( + 'attributeaggregator: AA entityId is not specified in the configuration.' + ); + } + + $aameta = null; + + $globalConfig = Configuration::getInstance(); + $metadataSources = $globalConfig->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 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 { + + if (!$aameta) { throw new Exception( - 'attributeaggregator: AA entityId is not specified in the configuration.' - ); + '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"]; } @@ -174,7 +191,7 @@ public function __construct(array $config, $reserved) */ public function process(array &$state): void { - assert('is_array($state)'); + Assert::is_array($state); $state['attributeaggregator:authsourceId'] = $state["saml:sp:State"]["saml:sp:AuthId"]; $state['attributeaggregator:entityId'] = $this->entityId; @@ -193,6 +210,7 @@ public function process(array &$state): void 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)); } + $id = State::saveState($state, 'attributeaggregator:request'); $url = Module::getModuleURL('attributeaggregator/attributequery.php'); $params = ['StateId' => $id]; From 46d4ce0e1d3dd3d755d319a3ec9fdbc1e5061866 Mon Sep 17 00:00:00 2001 From: Czier Norbert Date: Thu, 10 Apr 2025 13:47:49 +0200 Subject: [PATCH 3/4] Fix --- src/Auth/Process/AttributeAggregator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth/Process/AttributeAggregator.php b/src/Auth/Process/AttributeAggregator.php index 79065eb..3ec90ed 100644 --- a/src/Auth/Process/AttributeAggregator.php +++ b/src/Auth/Process/AttributeAggregator.php @@ -191,7 +191,7 @@ public function __construct(array $config, $reserved) */ 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; From a37aee6383039225a506d80bb9d0c1bc132aa757 Mon Sep 17 00:00:00 2001 From: Czier Norbert Date: Fri, 11 Apr 2025 10:24:29 +0200 Subject: [PATCH 4/4] Fix --- public/attributequery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/attributequery.php b/public/attributequery.php index 683c6f6..675acdb 100644 --- a/public/attributequery.php +++ b/public/attributequery.php @@ -44,7 +44,7 @@ $aaMetadata = $mdq->getMetaData($state['attributeaggregator:entityId'],'attributeauthority-remote'); if ($aaMetadata) { - if (array_keys($aaMetadata) !== range(0, count($aaMetadata) - 1)) { + if (array_keys($aaMetadata) === range(0, count($aaMetadata) - 1)) { $aaMetadata = $aaMetadata[0]; } break;