From b45df0729c5d9048d975a1bb7b6e77f58c54edf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Pe=CC=81rez?= <jaime.perez@uninett.no> Date: Fri, 29 Jul 2016 14:16:07 +0200 Subject: [PATCH] authproc: Add new filter to remove invalid scopes. The new saml:FilterScopes allows a SAML Service Provider to remove the values from a scoped attribute whose scope is not declared in the IdP metadata and/or does not match with the domain in use by the IdP itself. This closes #22. --- docs/simplesamlphp-authproc.md | 1 + modules/saml/docs/filterscopes.md | 71 ++++++++++++++ .../saml/lib/Auth/Process/FilterScopes.php | 97 +++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 modules/saml/docs/filterscopes.md create mode 100644 modules/saml/lib/Auth/Process/FilterScopes.php diff --git a/docs/simplesamlphp-authproc.md b/docs/simplesamlphp-authproc.md index e0211a527..784ae9e08 100644 --- a/docs/simplesamlphp-authproc.md +++ b/docs/simplesamlphp-authproc.md @@ -145,6 +145,7 @@ The following filters are included in the SimpleSAMLphp distribution: - [`preprodwarning:Warning`](./preprodwarning:warning): Warn the user about accessing a test IdP. - [`saml:AttributeNameID`](./saml:nameid): Generate custom NameID with the value of an attribute. - [`saml:ExpectedAuthnContextClassRef`](./saml:authproc_expectedauthncontextclassref): Verify the user's authentication context. +- [`saml:FilterScopes`](./saml:filterscopes): Filter attribute values with scopes forbidden for an IdP. - [`saml:NameIDAttribute`](./saml:nameidattribute): Create an attribute based on the NameID we receive from the IdP. - [`saml:PersistentNameID`](./saml:nameid): Generate persistent NameID from an attribute. - [`saml:PersistentNameID2TargetedID`](./saml:nameid): Store persistent NameID as eduPersonTargetedID. diff --git a/modules/saml/docs/filterscopes.md b/modules/saml/docs/filterscopes.md new file mode 100644 index 000000000..a3c28fa4f --- /dev/null +++ b/modules/saml/docs/filterscopes.md @@ -0,0 +1,71 @@ +Scoped Attributes Filtering +=========================== + +This document describes the **FilterScopes** attribute filter in the saml module. + +This filter allows a Service Provider to make sure the scopes included in the values +of certain attributes correspond to what the Identity Provider declares in its +metadata. If the IdP includes a list of scopes in the metadata, only those scopes will +be allowed. On the other hand, if no scopes are declared or the scope is not included +in the list of declared scopes, it will be matched against the domain used by the +SAML `SingleSignOnService` endpoint. This means the `example.com` scope will be +allowed in attributes received from an IdP whose `SingleSignOnService` endpoint +is located on the `example.com` top domain or any subdomain of that. Such scope will +be rejected though if the match with the IdP's endpoint does not happen at the top +level, like for example with `example.com.domain.net`. + +If you are configuring the metadata of an IdP manually, remember to add an array +to it with the key `scope`, containing the list of scopes expected from that entity. + +Configuration +------------- + +This filter can be configured in the `config/authsources.php` file, inside the +`authproc` array of the corresponding SAML authentication source in use. + +Note that this filter **can only be used with SAML authentication sources**. + +Here are the options available for the filter: + +`attributes` +: An array containing a list of attributes that are scoped and therefore should be evaluated. + Defaults to _eduPersonPrincipalName_ and _eduPersonScopedAffiliation_. + + +Examples +-------- + +Basic configuration: +```php + 'authproc' => array( + 90 => array( + 'class' => 'saml:FilterScopes', + ), + ), +``` + +Specify `mail` and `eduPersonPrincipalName` as scoped attributes: +```php + 'authproc' => array( + 90 => array( + 'class' => 'saml:FilterScopes', + 'attributes' => array( + 'mail', + 'eduPersonPrincipalName', + ), + ), + ), +``` + +Specify the same attributes in OID format: +```php + 'authproc' => array( + 90 => array( + 'class' => 'saml:FilterScopes', + 'attributes' => array( + 'urn:oid:0.9.2342.19200300.100.1.3', + 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', + ), + ), + ), +``` diff --git a/modules/saml/lib/Auth/Process/FilterScopes.php b/modules/saml/lib/Auth/Process/FilterScopes.php new file mode 100644 index 000000000..5457ae9c5 --- /dev/null +++ b/modules/saml/lib/Auth/Process/FilterScopes.php @@ -0,0 +1,97 @@ +<?php + +namespace SimpleSAML\Module\saml\Auth\Process; + +use SimpleSAML\Logger; + +/** + * Filter to remove attribute values which are not properly scoped. + * + * @author Adam Lantos, NIIF / Hungarnet + * @author Jaime PĂ©rez Crespo, UNINETT AS <jaime.perez@uninett.no> + * @package SimpleSAMLphp + */ +class FilterScopes extends \SimpleSAML_Auth_ProcessingFilter +{ + + /** + * Stores any pre-configured scoped attributes which come from the filter configuration. + */ + private $scopedAttributes = array( + 'eduPersonScopedAffiliation', + 'eduPersonPrincipalName' + ); + + + /** + * Constructor for the processing filter. + * + * @param array &$config Configuration for this filter. + * @param mixed $reserved For future use. + */ + public function __construct(&$config, $reserved) + { + parent::__construct($config, $reserved); + assert('is_array($config)'); + + if (array_key_exists('attributes', $config) && !empty($config['attributes'])) { + $this->scopedAttributes = $config['attributes']; + } + } + + + /** + * This method applies the filter, removing any values + * + * @param array &$request the current request + */ + public function process(&$request) + { + $src = $request['Source']; + if (!count($this->scopedAttributes)) { + // paranoia, should never happen + Logger::warning('No scoped attributes configured.'); + return; + } + $validScopes = array(); + if (array_key_exists('scope', $src) && is_array($src['scope']) && !empty($src['scope'])) { + $validScopes = $src['scope']; + } + + foreach ($this->scopedAttributes as $attribute) { + if (!isset($request['Attributes'][$attribute])) { + continue; + } + + $values = $request['Attributes'][$attribute]; + $newValues = array(); + foreach ($values as $value) { + $ep = \SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint($request['Source']['SingleSignOnService']); + $loc = $ep['Location']; + $host = parse_url($loc, PHP_URL_HOST); + if ($host === null) { + $host = ''; + } + $value_a = explode('@', $value, 2); + if (count($value_a) < 2) { + continue; // there's no scope + } + $scope = $value_a[1]; + if (in_array($scope, $validScopes, true)) { + $newValues[] = $value; + } elseif (strpos($host, $scope) === strlen($host) - strlen($scope)) { + $newValues[] = $value; + } else { + Logger::warning("Removing value '$value' for attribute '$attribute'. Undeclared scope."); + } + } + + if (empty($newValues)) { + Logger::warning("No suitable values for attribute '$attribute', removing it."); + unset($request['Attributes'][$attribute]); // remove empty attributes + } else { + $request['Attributes'][$attribute] = $newValues; + } + } + } +} -- GitLab