├── 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 |
--------------------------------------------------------------------------------