├── default-enable ├── .gitignore ├── docs └── attributeaggregator.txt ├── phpunit.xml ├── .travis.yml ├── composer.json ├── tests ├── lib │ └── Auth │ │ └── Process │ │ └── attributeaggregatorTest.php └── _autoload_modules.php ├── README.md ├── www └── attributequery.php └── lib └── Auth └── Process └── attributeaggregator.php /default-enable: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /docs/attributeaggregator.txt: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - hhvm 8 | matrix: 9 | allow_failures: 10 | - php: hhvm 11 | before_script: apt-get install php-soap; composer update --dev 12 | script: php vendor/phpunit/phpunit/phpunit.php 13 | notifications: 14 | slack: eduid:JJc9VL5htezKWr40wEUTug8K 15 | 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "niif/simplesamlphp-module-attributeaggregator", 3 | "description": "Attribute Aggregator implementation or SAML AttributeQuery", 4 | "type": "simplesamlphp-module", 5 | "require": { 6 | "simplesamlphp/composer-module-installer": "~1.1", 7 | "ext-soap": "*" 8 | }, 9 | "require-dev": { 10 | "simplesamlphp/simplesamlphp": ">=1.14", 11 | "phpunit/phpunit": "~3.7" 12 | }, 13 | "autoload-dev": { 14 | "files": ["tests/_autoload_modules.php"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/lib/Auth/Process/attributeaggregatorTest.php: -------------------------------------------------------------------------------- 1 | process($request); 16 | return $request; 17 | } 18 | 19 | public function testAny() 20 | { 21 | $this->assertTrue(true, 'Just for travis.yml test'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/_autoload_modules.php: -------------------------------------------------------------------------------- 1 | array( 30 | 'class' => 'attributeaggregator:attributeaggregator', 31 | 'entityId' => 'https://aa.example.com:8443/aa', 32 | 33 | /** 34 | * The subject of the attribute query. Default: urn:oid:1.3.6.1.4.1.5923.1.1.1.6 (eduPersonPrincipalName) 35 | */ 36 | //'attributeId' => 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', 37 | 38 | /** 39 | * If set to TRUE, the module will throw an exception if attributeId is not found. 40 | */ 41 | // 'required' => FALSE, 42 | 43 | /** 44 | * The format of attributeId. Default is 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' 45 | */ 46 | //'nameIdFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', 47 | 48 | 49 | /** 50 | * The name Format of the attribute names. 51 | */ 52 | //'attributeNameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 53 | 54 | /** 55 | * The requested attributes. If not present, we will get all 56 | * the attributes. The keys of the array is the attribute name in (''urn:oid'') format. 57 | * values: 58 | * the array of acceptable values. If not defined, the filter will accept all values. 59 | * multiSource: 60 | * merge: merge the existing and the new values, this is the default behaviour, 61 | * override: drop the existing values and set the values from AA, 62 | * keep: drop the new values from AA and keep the original values. 63 | */ 64 | // 'attributes' => array( 65 | // "urn:oid:attribute-OID-1" => array ( 66 | // "values" => array ("value1", "value2"), 67 | // "multiSource" => "override" 68 | // ), 69 | // "urn:oid:attribute-OID-2" => array ( 70 | // "multiSource" => "keep" 71 | // ), 72 | // "urn:oid:attribute-OID-3" => array ( 73 | // "values" => array ("value1", "value2"), 74 | // ), 75 | // "urn:oid:attribute-OID-4" => array () 76 | // ), 77 | 78 | ), 79 | 80 | 81 | Options 82 | ------- 83 | 84 | The following options can be used when configuring the '''attributeaggregation''' module 85 | 86 | `entityId` 87 | : The entityId of the Attribute Authority. The metadata of the AA must be in the 88 | attributeauthority-remote metadata set, otherwise you will get an error message. 89 | 90 | `attributeId` 91 | : This is the *Subject* in the issued AttributeQuery. The attribute must be previously 92 | resolved by an authproc module. The default attribute is urn:oid:1.3.6.1.4.1.5923.1.1.1.6 93 | (eduPersonPrincipalName). 94 | 95 | `attributeNameFormat` 96 | : The format of the NameID in the issued AttributeQuery. The default value is 97 | `urn:oasis:names:tc:SAML:2.0:attrname-format:uri`. 98 | 99 | `attributes` 100 | : You can list the expected attributes from the Attrubute Authority in the *attributes* 101 | array. The array contains key-value pairs, where the keys are attribute names in full 102 | federated (''urn:oid'') format and the values are arrays with the expected values for 103 | that attribute. If the value is an empty array, all the values of the attributes are 104 | resolved, otherwise only the matching ones. If the `attributes` option is not defined, 105 | every attribute is resolved from the response from the AA. 106 | -------------------------------------------------------------------------------- /www/attributequery.php: -------------------------------------------------------------------------------- 1 | getMetadata($state['attributeaggregator:entityId'],'attributeauthority-remote'); 16 | 17 | /* Find an AttributeService with SOAP binding */ 18 | $aas = $aaMetadata['AttributeService']; 19 | for ($i=0;$isetData('attributeaggregator:data', $dataId, $data, 3600); 46 | 47 | $nameId = array( 48 | 'Format' => $data['nameIdFormat'], 49 | 'Value' => $data['nameIdValue'], 50 | 'NameQualifier' => $data['nameIdQualifier'], 51 | 'SPNameQualifier' => $data['nameIdSPQualifier'], 52 | ); 53 | if (empty($nameId['NameQualifier'])) { 54 | $nameId['NameQualifier'] = NULL; 55 | } 56 | if (empty($nameId['SPNameQualifier'])) { 57 | $nameId['SPNameQualifier'] = NULL; 58 | } 59 | 60 | $attributes = $state['attributeaggregator:attributes']; 61 | $attributes_to_send = array(); 62 | foreach ($attributes as $name => $params) { 63 | if (array_key_exists('values', $params)){ 64 | $attributes_to_send[$name] = $params['values']; 65 | } 66 | else { 67 | $attributes_to_send[$name] = array(); 68 | } 69 | } 70 | 71 | $attributeNameFormat = $state['attributeaggregator:attributeNameFormat']; 72 | 73 | $authsource = SimpleSAML_Auth_Source::getById($state["attributeaggregator:authsourceId"]); 74 | $src = $authsource->getMetadata(); 75 | $dst = $metadata->getMetaDataConfig($state['attributeaggregator:entityId'],'attributeauthority-remote'); 76 | 77 | // Sending query 78 | try { 79 | $response = sendQuery($dataId, $data['url'], $nameId, $attributes_to_send, $attributeNameFormat, $src, $dst); 80 | } catch (Exception $e) { 81 | throw new SimpleSAML_Error_Exception('[attributeaggregator] Got an exception while performing attribute query. Exception: '.get_class($e).', message: '.$e->getMessage()); 82 | } 83 | 84 | $idpEntityId = $response->getIssuer(); 85 | if ($idpEntityId === NULL) { 86 | throw new SimpleSAML_Error_Exception('Missing issuer in response.'); 87 | } 88 | $assertions = sspmod_saml_Message::processResponse($src, $dst, $response); 89 | $attributes_from_aa = $assertions[0]->getAttributes(); 90 | $expected_attributes = $state['attributeaggregator:attributes']; 91 | // get attributes from response, and put it in the state. 92 | foreach ($attributes_from_aa as $name=>$values){ 93 | // expected? 94 | if (array_key_exists($name, $expected_attributes)){ 95 | // There is in the existing attributes? 96 | if(array_key_exists($name, $state['Attributes'])){ 97 | // has multiSource rule? 98 | if (! empty($expected_attributes[$name]['multiSource'])){ 99 | switch ($expected_attributes[$name]['multiSource']) { 100 | case 'override': 101 | $state['Attributes'][$name] = $values; 102 | break; 103 | case 'keep': 104 | continue; 105 | break; 106 | case 'merge': 107 | $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); 108 | break; 109 | } 110 | } 111 | // default: merge the attributes 112 | else { 113 | $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); 114 | } 115 | } 116 | // There is not in the existing attributes, create it. 117 | else { 118 | $state['Attributes'][$name] = $values; 119 | } 120 | } 121 | // not expected? Put it to attributes array. 122 | else { 123 | if (!empty($state['Attributes'][$name])){ 124 | $state['Attributes'][$name] = array_merge($state['Attributes'][$name],$values); 125 | } 126 | else 127 | $state['Attributes'][$name] = $values; 128 | } 129 | } 130 | 131 | SimpleSAML_Logger::debug('[attributeaggregator] - Attributes now:'.var_export($state['Attributes'],true)); 132 | SimpleSAML_Auth_ProcessingChain::resumeProcessing($state); 133 | exit; 134 | 135 | /** 136 | * build and send AttributeQuery 137 | */ 138 | function sendQuery($dataId, $url, $nameId, $attributes, $attributeNameFormat,$src,$dst) { 139 | assert('is_string($dataId)'); 140 | assert('is_string($url)'); 141 | assert('is_array($nameId)'); 142 | assert('is_array($attributes)'); 143 | 144 | SimpleSAML_Logger::debug('[attributeaggregator] - sending request'); 145 | 146 | $query = new SAML2_AttributeQuery(); 147 | $query->setRelayState($dataId); 148 | $query->setDestination($url); 149 | $query->setIssuer($src->getValue('entityid')); 150 | $query->setNameId($nameId); 151 | $query->setAttributeNameFormat($attributeNameFormat); 152 | if (! empty($attributes)){ 153 | $query->setAttributes($attributes); 154 | } 155 | sspmod_saml_Message::addSign($src,$dst,$query); 156 | 157 | if (! $query->getSignatureKey()){ 158 | throw new SimpleSAML_Error_Exception('[attributeaggregator] - Unable to find private key for signing attribute request.'); 159 | } 160 | 161 | SimpleSAML_Logger::debug('[attributeaggregator] - sending attribute query: '.var_export($query,1)); 162 | $binding = new SAML2_SOAPClient(); 163 | 164 | $result = $binding->send($query, $src, $dst); 165 | return $result; 166 | } 167 | -------------------------------------------------------------------------------- /lib/Auth/Process/attributeaggregator.php: -------------------------------------------------------------------------------- 1 | 8 | * @package simpleSAMLphp 9 | * @version $Id$ 10 | */ 11 | class sspmod_attributeaggregator_Auth_Process_attributeaggregator extends SimpleSAML_Auth_ProcessingFilter 12 | { 13 | 14 | /** 15 | * 16 | * AA IdP entityId 17 | * @var string 18 | */ 19 | private $entityId = null; 20 | 21 | /** 22 | * 23 | * attributeId, the key of the user in the AA. default is eduPersonPrincipalName 24 | * @var unknown_type 25 | */ 26 | private $attributeId = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6"; 27 | 28 | /** 29 | * 30 | * If set to TRUE, the module will throw an exception if attributeId is not found. 31 | * @var boolean 32 | */ 33 | private $required = FALSE; 34 | 35 | /** 36 | * 37 | * nameIdFormat, the format of the attributeId. Default is "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"; 38 | * @var unknown_type 39 | */ 40 | private $nameIdFormat = SAML2_Const::NAMEID_PERSISTENT; 41 | 42 | 43 | /** 44 | * Array of the requested attributes 45 | * @var array 46 | */ 47 | private $attributes = array(); 48 | 49 | /** 50 | * nameFormat of attributes. Default is "urn:oasis:names:tc:SAML:2.0:attrname-format:uri" 51 | * @var string 52 | */ 53 | private $attributeNameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"; 54 | 55 | /** 56 | * Initialize attributeaggregator filter 57 | * 58 | * Validates and parses the configuration 59 | * 60 | * @param array $config Configuration information 61 | * @param mixed $reserved For future use 62 | */ 63 | public function __construct($config, $reserved) 64 | { 65 | assert('is_array($config)'); 66 | parent::__construct($config, $reserved); 67 | 68 | $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 69 | 70 | if ($config['entityId']) { 71 | $aameta = $metadata->getMetaData($config['entityId'], 'attributeauthority-remote'); 72 | if (!$aameta) { 73 | throw new SimpleSAML_Error_Exception( 74 | 'attributeaggregator: AA entityId (' . $config['entityId'] . 75 | ') does not exist in the attributeauthority-remote metadata set.' 76 | ); 77 | } 78 | $this->entityId = $config['entityId']; 79 | } 80 | else { 81 | throw new SimpleSAML_Error_Exception( 82 | 'attributeaggregator: AA entityId is not specified in the configuration.' 83 | ); 84 | } 85 | 86 | if (! empty($config["attributeId"])){ 87 | $this->attributeId = $config["attributeId"]; 88 | } 89 | 90 | if (! empty($config["required"])){ 91 | $this->required = $config["required"]; 92 | } 93 | 94 | if (!empty($config["nameIdFormat"])){ 95 | foreach (array( 96 | SAML2_Const::NAMEID_UNSPECIFIED, 97 | SAML2_Const::NAMEID_PERSISTENT, 98 | SAML2_Const::NAMEID_TRANSIENT, 99 | SAML2_Const::NAMEID_ENCRYPTED) as $format) { 100 | $invalid = TRUE; 101 | if ($config["nameIdFormat"] == $format) { 102 | $this->nameIdFormat = $config["nameIdFormat"]; 103 | $invalid = FALSE; 104 | break; 105 | } 106 | } 107 | if ($invalid) 108 | throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid nameIdFormat: ".$config["nameIdFormat"]); 109 | } 110 | 111 | if (!empty($config["attributes"])){ 112 | if (! is_array($config["attributes"])) { 113 | throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid format of attributes array in the configuration"); 114 | } 115 | foreach ($config["attributes"] as $attribute) { 116 | if (! is_array($attribute)) { 117 | throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid format of attributes array in the configuration"); 118 | } 119 | if (array_key_exists("values", $attribute)) { 120 | if (! is_array($attribute["values"])) { 121 | throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid format of attributes array in the configuration"); 122 | } 123 | } 124 | if (array_key_exists('multiSource', $attribute)){ 125 | if(! preg_match('/^(merge|keep|override)$/', $attribute['multiSource'])) 126 | throw new SimpleSAML_Error_Exception( 127 | 'attributeaggregator: Invalid multiSource value '.$attribute['multiSource'].' for '.key($attribute).'. It not mached keep, merge or override.' 128 | ); 129 | } 130 | } 131 | $this->attributes = $config["attributes"]; 132 | } 133 | 134 | if (!empty($config["attributeNameFormat"])){ 135 | foreach (array( 136 | SAML2_Const::NAMEFORMAT_UNSPECIFIED, 137 | SAML2_Const::NAMEFORMAT_URI, 138 | SAML2_Const::NAMEFORMAT_BASIC) as $format) { 139 | $invalid = TRUE; 140 | if ($config["attributeNameFormat"] == $format) { 141 | $this->attributeNameFormat = $config["attributeNameFormat"]; 142 | $invalid = FALSE; 143 | break; 144 | } 145 | } 146 | if ($invalid) 147 | throw new SimpleSAML_Error_Exception("attributeaggregator: Invalid attributeNameFormat: ".$config["attributeNameFormat"], 1); 148 | } 149 | } 150 | 151 | /** 152 | * Process a authentication response 153 | * 154 | * This function saves the state, and redirects the user to the Attribute Authority for 155 | * entitlements. 156 | * 157 | * @param array &$state The state of the response. 158 | * 159 | * @return void 160 | */ 161 | public function process(&$state) 162 | { 163 | assert('is_array($state)'); 164 | $state['attributeaggregator:authsourceId'] = $state["saml:sp:State"]["saml:sp:AuthId"]; 165 | $state['attributeaggregator:entityId'] = $this->entityId; 166 | 167 | $state['attributeaggregator:attributeId'] = $state['Attributes'][$this->attributeId]; 168 | $state['attributeaggregator:nameIdFormat'] = $this->nameIdFormat; 169 | 170 | $state['attributeaggregator:attributes'] = $this->attributes; 171 | $state['attributeaggregator:attributeNameFormat'] = $this->attributeNameFormat; 172 | 173 | if (! $state['attributeaggregator:attributeId']){ 174 | if (! $this->required) { 175 | SimpleSAML_Logger::info('[attributeaggregator] This user session does not have '.$this->attributeId.', which is required for querying the AA! Continue processing.'); 176 | SimpleSAML_Logger::debug('[attributeaggregator] Attributes are: '.var_export($state['Attributes'],true)); 177 | SimpleSAML_Auth_ProcessingChain::resumeProcessing($state); 178 | } 179 | 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)); 180 | } 181 | 182 | // Save state and redirect 183 | $id = SimpleSAML_Auth_State::saveState($state, 'attributeaggregator:request'); 184 | $url = SimpleSAML_Module::getModuleURL('attributeaggregator/attributequery.php'); 185 | SimpleSAML_Utilities::redirect($url, array('StateId' => $id)); // FIXME: redirect is deprecated 186 | } 187 | } 188 | --------------------------------------------------------------------------------