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