From e2f779bbc70a53a687f60171980b900cf65fb052 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Mon, 25 Aug 2008 11:42:53 +0000
Subject: [PATCH] Consent module.

This implements a module for requesting attribute release consent from the
user. It is based on the consent support already in simpleSAMLphp. There are
some changes brom the builtin consent support. The database schema is
different, the table now includes a UNIQUE constraint on user and destination,
and the destination is no longer encoded as a hash.

There is also support for saving the consent in cookies.

See modules/consent/Auth/Process/Consent.php for information about
configuration.


git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@829 44740490-163a-0410-bde0-09ae8108e29a
---
 modules/consent/default-disable               |   3 +
 modules/consent/lib/Auth/Process/Consent.php  | 145 +++++++
 modules/consent/lib/Consent/Store/Cookie.php  | 247 ++++++++++++
 .../consent/lib/Consent/Store/Database.php    | 378 ++++++++++++++++++
 modules/consent/lib/Store.php                 | 107 +++++
 .../consent/templates/default/consentform.php | 117 ++++++
 .../consent/templates/default/noconsent.php   |  21 +
 modules/consent/www/getconsent.php            |  82 ++++
 modules/consent/www/noconsent.php             |  27 ++
 9 files changed, 1127 insertions(+)
 create mode 100644 modules/consent/default-disable
 create mode 100644 modules/consent/lib/Auth/Process/Consent.php
 create mode 100644 modules/consent/lib/Consent/Store/Cookie.php
 create mode 100644 modules/consent/lib/Consent/Store/Database.php
 create mode 100644 modules/consent/lib/Store.php
 create mode 100644 modules/consent/templates/default/consentform.php
 create mode 100644 modules/consent/templates/default/noconsent.php
 create mode 100644 modules/consent/www/getconsent.php
 create mode 100644 modules/consent/www/noconsent.php

diff --git a/modules/consent/default-disable b/modules/consent/default-disable
new file mode 100644
index 000000000..fa0bd82e2
--- /dev/null
+++ b/modules/consent/default-disable
@@ -0,0 +1,3 @@
+This file indicates that the default state of this module
+is disabled. To enable, create a file named enable in the
+same directory as this file.
diff --git a/modules/consent/lib/Auth/Process/Consent.php b/modules/consent/lib/Auth/Process/Consent.php
new file mode 100644
index 000000000..e95b2aff0
--- /dev/null
+++ b/modules/consent/lib/Auth/Process/Consent.php
@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * Filter for requiring the user to give consent before the attributes are released to the SP.
+ *
+ * The initial focus of the consent form can be set by setting the 'focus'-attribute to either
+ * 'yes' or 'no'.
+ *
+ * Different storage backends can be configured by setting the 'store'-attribute. The'store'-attribute
+ * is on the form <module>:<class>, and refers to the class sspmod_<module>_Consent_Store_<class>. For
+ * examples, see the built-in modules 'consent:Cookie' and 'consent:Database', which can be found
+ * under modules/consent/lib/Consent/Store.
+ *
+ * Example - minimal:
+ * <code>
+ * 'authproc' => array(
+ *   'consent:Consent',
+ *   ),
+ * </code>
+ *
+ * Example - save in cookie:
+ * <code>
+ * 'authproc' => array(
+ *   array(
+ *     'consent:Consent',
+ *     'store' => 'consent:Cookie',
+ *   ),
+ * </code>
+ *
+ * Example - save in MySQL database:
+ * <code>
+ * 'authproc' => array(
+ *   array(
+ *     'consent:Consent',
+ *     'store' => array(
+ *       'consent:Database',
+ *       'dsn' => 'mysql:host=db.example.org;dbname=simplesaml',
+ *       'username' => 'simplesaml',
+ *       'password' => 'secretpassword',
+ *       ),
+ *     ),
+ *   ),
+ * </code>
+ *
+ * Example - initial focus on yes-button:
+ * <code>
+ * 'authproc' => array(
+ *   array('consent:Consent', 'focus' => 'yes'),
+ *   ),
+ * </code>
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_consent_Auth_Process_Consent extends SimpleSAML_Auth_ProcessingFilter {
+
+	/**
+	 * Where the focus should be in the form. Can be 'yesbutton', 'nobutton', or NULL.
+	 */
+	private $focus;
+
+
+	/**
+	 * Consent store, if enabled.
+	 */
+	private $store;
+
+
+	/**
+	 * Initialize consent filter.
+	 *
+	 * This is the constructor for the consent filter. It validates and parses the configuration.
+	 *
+	 * @param array $config  Configuration information about 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('focus', $config)) {
+			$this->focus = $config['focus'];
+			if (!in_array($this->focus, array('yes', 'no'), TRUE)) {
+				throw new Exception('Invalid value for \'focus\'-parameter to' .
+					' consent:Consent authentication filter: ' . var_export($this->focus, TRUE));
+			}
+		} else {
+			$this->focus = NULL;
+		}
+
+		if (array_key_exists('store', $config)) {
+			$this->store = sspmod_consent_Store::parseStoreConfig($config['store']);
+		} else {
+			$this->store = NULL;
+		}
+
+	}
+
+
+	/**
+	 * Process a authentication response.
+	 *
+	 * This function saves the state, and redirects the user to the page where the user
+	 * can authorize the release of the attributes.
+	 *
+	 * @param array $state  The state of the response.
+	 */
+	public function process(&$state) {
+		assert('is_array($state)');
+		assert('array_key_exists("UserID", $state)');
+		assert('array_key_exists("Destination", $state)');
+		assert('array_key_exists("entityid", $state["Destination"])');
+		assert('array_key_exists("metadata-set", $state["Destination"])');
+
+		if ($this->store !== NULL) {
+			$userId = sha1($state['UserID'] . SimpleSAML_Utilities::getSecretSalt());;
+			$destination = $state['Destination']['metadata-set'] . '/' . $state['Destination']['entityid'];
+
+			$attributeSet = array_keys($state['Attributes']);
+			sort($attributeSet);
+			$attributeSet = implode(',', $attributeSet);
+			$attributeSet = sha1($attributeSet);
+
+			if ($this->store->hasConsent($userId, $destination, $attributeSet)) {
+				/* Consent already given. */
+				return;
+			}
+
+			$state['consent:store'] = $this->store;
+			$state['consent:store.userId'] = $userId;
+			$state['consent:store.destination'] = $destination;
+			$state['consent:store.attributeSet'] = $attributeSet;
+		}
+
+		$state['consent:focus'] = $this->focus;
+
+		/* Save state and redirect. */
+		$id = SimpleSAML_Auth_State::saveState($state, 'consent:request');
+		$url = SimpleSAML_Module::getModuleURL('consent/getconsent.php');
+		SimpleSAML_Utilities::redirect($url, array('StateId' => $id));
+	}
+
+}
+
+?>
\ No newline at end of file
diff --git a/modules/consent/lib/Consent/Store/Cookie.php b/modules/consent/lib/Consent/Store/Cookie.php
new file mode 100644
index 000000000..4d96f8930
--- /dev/null
+++ b/modules/consent/lib/Consent/Store/Cookie.php
@@ -0,0 +1,247 @@
+<?php
+
+/**
+ * Store consent in cookies.
+ *
+ * This class implements a consent store which stores the consent information in
+ * cookies on the users computer.
+ *
+ * Example - Consent module with cookie store:
+ * <code>
+ * 'authproc' => array(
+ *   array(
+ *     'consent:Consent',
+ *     'store' => 'consent:Cookie',
+ *     ),
+ *   ),
+ * </code>
+ *
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_consent_Consent_Store_Cookie extends sspmod_consent_Store {
+
+
+	/**
+	 * Check for consent.
+	 *
+	 * This function checks whether a given user has authorized the release of the attributes
+	 * identified by $attributeSet from $source to $destination.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifies the destination.
+	 * @param string $attributeSet  A hash which identifies the attributes.
+	 * @return bool  TRUE if the user has given consent earlier, FALSE if not (or on error).
+	 */
+	public function hasConsent($userId, $destinationId, $attributeSet) {
+		assert('is_string($userId)');
+		assert('is_string($destinationId)');
+		assert('is_string($attributeSet)');
+
+		$cookieName = self::getCookieName($userId, $destinationId);
+
+		if (!array_key_exists($cookieName, $_COOKIE)) {
+			SimpleSAML_Logger::debug('Consent cookie - no cookie with name \'' . $cookieName . '\'.');
+			return FALSE;
+		}
+		if (!is_string($_COOKIE[$cookieName])) {
+			SimpleSAML_Logger::warning('Value of consent cookie wasn\'t a string. Was: ' . var_export($_COOKIE[$cookieName], TRUE));
+			return FALSE;
+		}
+
+		$data = $userId . ':' . $attributeSet . ':' . $destinationId;
+		$data = self::sign($data);
+
+		if ($_COOKIE[$cookieName] !== $data) {
+			SimpleSAML_Logger::info('Attribute set changed from the last time consent was given.');
+			return FALSE;
+		}
+
+		SimpleSAML_Logger::debug('Consent cookie - found cookie with correct name and value.');
+
+		return TRUE;
+	}
+
+
+	/**
+	 * Save consent.
+	 *
+	 * Called when the user asks for the consent to be saved. If consent information
+	 * for the given user and destination already exists, it should be overwritten.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifies the destination.
+	 * @param string $attributeSet  A hash which identifies the attributes.
+	 */
+	public function saveConsent($userId, $destinationId, $attributeSet) {
+		assert('is_string($userId)');
+		assert('is_string($destinationId)');
+		assert('is_string($attributeSet)');
+
+		$name = self::getCookieName($userId, $destinationId);
+		$value = $userId . ':' . $attributeSet . ':' . $destinationId;
+		$value = self::sign($value);
+		$this->setConsentCookie($name, $value);
+	}
+
+
+	/**
+	 * Delete consent.
+	 *
+	 * Called when a user revokes consent for a given destination.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifies the destination.
+	 */
+	public function deleteConsent($userId, $destinationId) {
+		assert('is_string($userId)');
+		assert('is_string($destinationId)');
+
+		$name = self::getCookieName($userId, $destinationId);
+		$this->setConsentCookie($name, NULL);
+
+	}
+
+
+	/**
+	 * Retrieve consents.
+	 *
+	 * This function should return a list of consents the user has saved.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @return array  Array of all destination ids the user has given consent for.
+	 */
+	public function getConsents($userId) {
+		assert('is_string($userId)');
+
+		$ret = array();
+
+		$cookieNameStart = 'sspmod_consent:';
+		$cookieNameStartLen = strlen($cookieNameStart);
+		foreach ($_COOKIE as $name => $value) {
+
+			if (substr($name, 0, $cookieNameStartLen) !== $cookieNameStart) {
+				continue;
+			}
+
+			$value = self::verify($value);
+			if ($value === FALSE) {
+				continue;
+			}
+
+			$tmp = explode(':', $value, 3);
+			if (count($tmp) !== 3) {
+				SimpleSAML_Logger::warning('Consent cookie with invalid value: ' . $value);
+				continue;
+			}
+
+			if ($userId !== $tmp[0]) {
+				/* Wrong user. */
+				continue;
+			}
+
+			$destination = $tmp[2];
+
+
+			$ret[] = $destination;
+		}
+
+		return $ret;
+	}
+
+
+	/**
+	 * Calculate a signature of some data.
+	 *
+	 * This function calculates a signature of the data.
+	 *
+	 * @param string $data  The data which should be signed.
+	 * @return string  The signed data.
+	 */
+	private static function sign($data) {
+		assert('is_string($data)');
+
+		$secretSalt = SimpleSAML_Utilities::getSecretSalt();
+
+		return sha1($secretSalt . $data . $secretSalt) . ':' . $data;
+	}
+
+
+	/**
+	 * Verify signed data.
+	 *
+	 * This function verifies signed data.
+	 *
+	 * @param string $signedData  The data which is signed.
+	 * @return string|FALSE  The data, or FALSE if the signature is invalid.
+	 */
+	private static function verify($signedData) {
+		assert('is_string($signedData)');
+
+		$data = explode(':', $signedData, 2);
+		if (count($data) !== 2) {
+			SimpleSAML_Logger::warning('Consent cookie: Missing signature.');
+			return FALSE;
+		}
+		$data = $data[1];
+
+		$newSignedData = self::sign($data);
+		if ($newSignedData !== $signedData) {
+			SimpleSAML_Logger::warning('Consent cookie: Invalid signature.');
+			return FALSE;
+		}
+
+		return $data;
+	}
+
+
+	/**
+	 * Get cookie name.
+	 *
+	 * This function gets the cookie name for the given user & destination.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifies the destination.
+	 */
+	private static function getCookieName($userId, $destinationId) {
+		assert('is_string($userId)');
+		assert('is_string($destinationId)');
+
+		return 'sspmod_consent:' . sha1($userId . ':' . $destinationId);
+	}
+
+
+	/**
+	 * Helper function for setting a cookie.
+	 *
+	 * @param string $name  Name of the cookie.
+	 * @param string|NULL $value  Value of the cookie. Set this to NULL to delete the cookie.
+	 */
+	private function setConsentCookie($name, $value) {
+		assert('is_string($name)');
+		assert('is_string($value)');
+
+		if ($value === NULL) {
+			$expire = 1; /* Delete by setting expiry in the past. */
+			$value = '';
+		} else {
+			$expire = time() + 90 * 24*60*60;
+		}
+
+		if (SimpleSAML_Utilities::isHTTPS()) {
+			/* Enable secure cookie for https-requests. */
+			$secure = TRUE;
+		} else {
+			$secure = FALSE;
+		}
+
+		$globalConfig = SimpleSAML_Configuration::getInstance();
+		$path = '/' . $globalConfig->getBaseURL();
+
+		setcookie($name, $value, $expire, $path, NULL, $secure);
+	}
+
+}
+
+?>
\ No newline at end of file
diff --git a/modules/consent/lib/Consent/Store/Database.php b/modules/consent/lib/Consent/Store/Database.php
new file mode 100644
index 000000000..337713aa4
--- /dev/null
+++ b/modules/consent/lib/Consent/Store/Database.php
@@ -0,0 +1,378 @@
+<?php
+
+/**
+ * Store consent in database.
+ *
+ * This class implements a consent store which stores the consent information in
+ * a database. It is tested, and should work against both MySQL and PostgreSQL.
+ *
+ * It has the following options:
+ * - dsn: The DSN which should be used to connect to the database server. Check the various
+ *        database drivers in http://php.net/manual/en/pdo.drivers.php for a description of
+ *        the various DSN formats.
+ * - username: The username which should be used when connecting to the database server.
+ * - password: The password which should be used when connecting to the database server.
+ * - table: The name of the table. Optional, defaults to 'ssp_consent'.
+ *
+ * Example - consent module with MySQL database:
+ * <code>
+ * 'authproc' => array(
+ *   array(
+ *     'consent:Consent',
+ *     'store' => array(
+ *       'consent:Database',
+ *       'dsn' => 'mysql:host=db.example.org;dbname=simplesaml',
+ *       'username' => 'simplesaml',
+ *       'password' => 'secretpassword',
+ *       ),
+ *     ),
+ *   ),
+ * </code>
+ *
+ * Example - consent module with PostgreSQL database:
+ * <code>
+ * 'authproc' => array(
+ *   array(
+ *     'consent:Consent',
+ *     'store' => array(
+ *       'consent:Database',
+ *       'dsn' => 'pgsql:host=db.example.org;port=5432;dbname=simplesaml',
+ *       'username' => 'simplesaml',
+ *       'password' => 'secretpassword',
+ *       ),
+ *     ),
+ *   ),
+ * </code>
+ *
+ *
+ * Table declaration:
+ * CREATE TABLE ssp_consent (
+ *   consentTime TIMESTAMP NOT NULL,
+ *   lastUse TIMESTAMP NOT NULL,
+ *   userId VARCHAR(80) NOT NULL,
+ *   destinationId VARCHAR(255) NOT NULL,
+ *   attributeSet VARCHAR(80) NOT NULL,
+ *   UNIQUE (userId, destinationId)
+ * );
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_consent_Consent_Store_Database extends sspmod_consent_Store {
+
+
+	/**
+	 * DSN for the database.
+	 */
+	private $dsn;
+
+
+	/**
+	 * Username for the database.
+	 */
+	private $username;
+
+
+	/**
+	 * Password for the database;
+	 */
+	private $password;
+
+
+	/**
+	 * Table with consent.
+	 */
+	private $table;
+
+
+	/**
+	 * Database handle.
+	 *
+	 * This variable can't be serialized.
+	 */
+	private $db;
+
+
+	/**
+	 * Parse configuration.
+	 *
+	 * This constructor parses the configuration.
+	 *
+	 * @param array $config  Configuration for database consent store.
+	 */
+	public function __construct($config) {
+		parent::__construct($config);
+
+		foreach (array('dsn', 'username', 'password') as $id) {
+			if (!array_key_exists($id, $config)) {
+				throw new Exception('consent:Database - Missing required option \'' . $id . '\'.');
+			}
+			if (!is_string($config[$id])) {
+				throw new Exception('consent:Database - \'' . $id . '\' is supposed to be a string.');
+			}
+		}
+
+		$this->dsn = $config['dsn'];
+		$this->username = $config['username'];
+		$this->password = $config['password'];
+
+		if (array_key_exists('table', $config)) {
+			if (!is_string($config['table'])) {
+				throw new Exception('consent:Database - \'table\' is supposed to be a string.');
+			}
+			$this->table = $config['table'];
+		} else {
+			$this->table = 'ssp_consent';
+		}
+	}
+
+
+	/**
+	 * Called before serialization.
+	 *
+	 * @return array  The variables which should be serialized.
+	 */
+	public function __sleep() {
+
+		return array(
+			'dsn',
+			'username',
+			'password',
+			'table',
+			);
+	}
+
+
+	/**
+	 * Check for consent.
+	 *
+	 * This function checks whether a given user has authorized the release of the attributes
+	 * identified by $attributeSet from $source to $destination.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifies the destination.
+	 * @param string $attributeSet  A hash which identifies the attributes.
+	 * @return bool  TRUE if the user has given consent earlier, FALSE if not (or on error).
+	 */
+	public function hasConsent($userId, $destinationId, $attributeSet) {
+		assert('is_string($userId)');
+		assert('is_string($destinationId)');
+		assert('is_string($attributeSet)');
+
+		$st = $this->execute('UPDATE ' . $this->table . ' SET lastUse = NOW() WHERE userId = ? AND destinationId = ? AND attributeSet = ?',
+			array($userId, $destinationId, $attributeSet));
+		if ($st === FALSE) {
+			return FALSE;
+		}
+
+		$rowCount = $st->rowCount();
+		if ($rowCount === 0) {
+			SimpleSAML_Logger::debug('consent:Database - No consent found.');
+			return FALSE;
+		} else {
+			SimpleSAML_Logger::debug('consent:Database - Consent found.');
+			return TRUE;
+		}
+
+	}
+
+
+	/**
+	 * Save consent.
+	 *
+	 * Called when the user asks for the consent to be saved. If consent information
+	 * for the given user and destination already exists, it should be overwritten.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifies the destination.
+	 * @param string $attributeSet  A hash which identifies the attributes.
+	 */
+	public function saveConsent($userId, $destinationId, $attributeSet) {
+		assert('is_string($userId)');
+		assert('is_string($destinationId)');
+		assert('is_string($attributeSet)');
+
+		/* Check for old consent (with different attribute set). */
+		$st = $this->execute('UPDATE ' . $this->table . ' SET consentTime = NOW(), lastUse = NOW(), attributeSet = ? WHERE userId = ? AND destinationId = ?',
+			array($attributeSet, $userId, $destinationId));
+		if ($st === FALSE) {
+			return;
+		}
+		if ($st->rowCount() > 0) {
+			/* We had already stored consent for the given destination in the database. */
+			SimpleSAML_Logger::debug('consent:Database - Updated old consent.');
+			return;
+		}
+
+		/* Add new consent. We don't check for error since there is nothing we can do if one occurs. */
+		$st = $this->execute('INSERT INTO ' . $this->table . ' (consentTime, lastUse, userId, destinationId, attributeSet) VALUES(NOW(),NOW(),?,?,?)',
+			array($userId, $destinationId, $attributeSet));
+		if ($st !== FALSE) {
+			SimpleSAML_Logger::debug('consent:Database - Saved new consent.');
+		}
+	}
+
+
+	/**
+	 * Delete consent.
+	 *
+	 * Called when a user revokes consent for a given destination.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifies the destination.
+	 */
+	public function deleteConsent($userId, $destinationId) {
+		assert('is_string($userId)');
+		assert('is_string($destinationId)');
+
+		$st = $this->execute('DELETE FROM ' . $this->table . ' WHERE userId = ? and destinationId = ?',
+			array($userId, $destinationId));
+		if ($st === FALSE) {
+			return;
+		}
+
+		if ($st->rowCount() > 0) {
+			SimpleSAML_Logger::debug('consent:Database - Deleted consent.');
+		} else {
+			SimpleSAML_Logger::warning('consent:Database - Attempted to delete nonexistent consent');
+		}
+	}
+
+
+	/**
+	 * Retrieve consents.
+	 *
+	 * This function should return a list of consents the user has saved.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @return array  Array of all destination ids the user has given consent for.
+	 */
+	public function getConsents($userId) {
+		assert('is_string($userId)');
+
+		$ret = array();
+
+		$st = $this->execute('SELECT destinationId FROM ' . $this->table . ' WHERE userId = ?',
+			array($userId));
+		if ($st === FALSE) {
+			return array();
+		}
+
+		while ($row = $st->fetch(PDO::FETCH_NUM)) {
+			$ret[] = $row[0];
+		}
+
+		return $ret;
+	}
+
+
+	/**
+	 * Prepare and execute statement.
+	 *
+	 * This function prepares and executes a statement. On error, FALSE will be returned.
+	 *
+	 * @param string $statement  The statement which should be executed.
+	 * @param array $parameters  Parameters for the statement.
+	 * @return PDOStatement|FALSE  The statement, or FALSE if execution failed.
+	 */
+	private function execute($statement, $parameters) {
+		assert('is_string($statement)');
+		assert('is_array($parameters)');
+
+		$db = $this->getDB();
+		if ($db === FALSE) {
+			return FALSE;
+		}
+
+		$st = $db->prepare($statement);
+		if ($st === FALSE) {
+			if ($st === FALSE) {
+				SimpleSAML_Logger::error('consent:Database - Error preparing statement \'' .
+					$statement . '\': ' . self::formatError($db->errorInfo()));
+				return FALSE;
+			}
+		}
+
+		if ($st->execute($parameters) !== TRUE) {
+			SimpleSAML_Logger::error('consent:Database - Error executing statement \'' .
+				$statement . '\': ' . self::formatError($st->errorInfo()));
+			return FALSE;
+		}
+
+		return $st;
+	}
+
+
+	/**
+	 * Create consent table.
+	 *
+	 * This function creates the table with consent data.
+	 *
+	 * @return TRUE if successful, FALSE if not.
+	 */
+	private function createTable() {
+
+		$db = $this->getDB();
+		if ($db === FALSE) {
+			return FALSE;
+		}
+
+		$res = $this->db->exec(
+			'CREATE TABLE ' . $this->table . ' (' .
+			'consentTime TIMESTAMP NOT NULL,' .
+			'lastUse TIMESTAMP NOT NULL,' .
+			'userId VARCHAR(80) NOT NULL,' .
+			'destinationId VARCHAR(255) NOT NULL,' .
+			'attributeSet VARCHAR(80) NOT NULL,' .
+			'UNIQUE (userId, destinationId)' .
+			')');
+		if ($res === FALSE) {
+			SimpleSAML_Logger::error('consent:Database - Failed to create table \'' . $this->table . '\'.');
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+
+	/**
+	 * Get database handle.
+	 *
+	 * @return PDO|FALSE  Database handle, or FALSE if we fail to connect.
+	 */
+	private function getDB() {
+
+		if ($this->db !== NULL) {
+			return $this->db;
+		}
+
+		try {
+			$this->db = new PDO($this->dsn, $this->username, $this->password);
+		} catch (PDOException $e) {
+			SimpleSAML_Logger::error('consent:Database - Failed to connect to \'' .
+				$this->dsn . '\': '. $e->getMessage());
+			$this->db = FALSE;
+		}
+
+		return $this->db;
+	}
+
+
+	/**
+	 * Format PDO error.
+	 *
+	 * This function formats a PDO error, as returned from errorInfo.
+	 *
+	 * @param array $error  The error information.
+	 * @return string  Error text.
+	 */
+	private static function formatError($error) {
+		assert('is_array($error)');
+		assert('count($error) >= 3');
+
+		return $error[0] . ' - ' . $error[2] . ' (' . $error[1] . ')';
+	}
+
+}
+
+?>
\ No newline at end of file
diff --git a/modules/consent/lib/Store.php b/modules/consent/lib/Store.php
new file mode 100644
index 000000000..c4ffe233f
--- /dev/null
+++ b/modules/consent/lib/Store.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * Base class for consent storage handlers.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+abstract class sspmod_consent_Store {
+
+
+	/**
+	 * Constructor for the base class.
+	 *
+	 * This constructor should always be called first in any class which implements
+	 * this class.
+	 *
+	 * @param array &$config  The configuration for this storage handler..
+	 */
+	protected function __construct(&$config) {
+		assert('is_array($config)');
+	}
+
+
+	/**
+	 * Check for consent.
+	 *
+	 * This function checks whether a given user has authorized the release of the attributes
+	 * identified by $attributeSet from $source to $destination.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifyes the destination.
+	 * @param string $attributeSet  A hash which identifies the attributes.
+	 * @return bool  TRUE if the user has given consent earlier, FALSE if not (or on error).
+	 */
+	abstract public function hasConsent($userId, $destinationId, $attributeSet);
+
+
+	/**
+	 * Save consent.
+	 *
+	 * Called when the user asks for the consent to be saved. If consent information
+	 * for the given user and destination already exists, it should be overwritten.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifyes the destination.
+	 * @param string $attributeSet  A hash which identifies the attributes.
+	 */
+	abstract public function saveConsent($userId, $destinationId, $attributeSet);
+
+
+	/**
+	 * Delete consent.
+	 *
+	 * Called when a user revokes consent for a given destination.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @param string $destinationId  A string which identifyes the destination.
+	 */
+	abstract public function deleteConsent($userId, $destinationId);
+
+
+	/**
+	 * Retrieve consents.
+	 *
+	 * This function should return a list of consents the user has saved.
+	 *
+	 * @param string $userId  The hash identifying the user at an IdP.
+	 * @return array  Array of all destination ids the user has given consent for.
+	 */
+	abstract public function getConsents($userId);
+
+
+	/**
+	 * Parse consent storage configuration.
+	 *
+	 * This function parses the configuration for a consent storage method. An exception
+	 * will be thrown if configuration parsing fails.
+	 *
+	 * @param mixed $config  The configuration.
+	 * @return sspmod_consent_Store  An object which implements of the sspmod_consent_Store class.
+	 */
+	public static function parseStoreConfig($config) {
+
+		if (is_string($config)) {
+			$config = array($config);
+		}
+
+		if (!is_array($config)) {
+			throw new Exception('Invalid configuration for consent store option: ' .
+				var_export($config, TRUE));
+		}
+
+		if (!array_key_exists(0, $config)) {
+			throw new Exception('Consent store without name given.');
+		}
+
+		$className = SimpleSAML_Module::resolveClass($config[0], 'Consent_Store',
+			'sspmod_consent_Store');
+
+		unset($config[0]);
+		return new $className($config);
+	}
+
+}
+
+?>
\ No newline at end of file
diff --git a/modules/consent/templates/default/consentform.php b/modules/consent/templates/default/consentform.php
new file mode 100644
index 000000000..6931dfd43
--- /dev/null
+++ b/modules/consent/templates/default/consentform.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * Template form for giving consent.
+ *
+ * Parameters:
+ * - 'srcMetadata': Metadata/configuration for the source.
+ * - 'dstMetadata': Metadata/configuration for the destination.
+ * - 'yesTarget': Target URL for the yes-button. This URL will receive a POST request.
+ * - 'yesData': Parameters which should be included in the yes-request.
+ * - 'noTarget': Target URL for the no-button. This URL will receive a GET request.
+ * - 'noData': Parameters which should be included in the no-request.
+ * - 'attributes': The attributes which are about to be released.
+ * - 'sppp': URL to the privacy policy of the destination, or FALSE.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+assert('is_array($this->data["srcMetadata"])');
+assert('is_array($this->data["dstMetadata"])');
+assert('is_string($this->data["yesTarget"])');
+assert('is_array($this->data["yesData"])');
+assert('is_string($this->data["noTarget"])');
+assert('is_array($this->data["noData"])');
+assert('is_array($this->data["attributes"])');
+assert('$this->data["sppp"] === FALSE || is_string($this->data["spp"])');
+
+
+/* Parse parameters. */
+
+if (array_key_exists('name', $this->data['srcMetadata'])) {
+	$srcName = $this->data['srcMetadata']['name'];
+	if (is_array($srcName)) {
+		$srcName = $this->t($srcName);
+	}
+} else {
+	$srcName = $this->data['srcMetadata']['entityid'];
+}
+
+if (array_key_exists('name', $this->data['dstMetadata'])) {
+	$dstName = $this->data['dstMetadata']['name'];
+	if (is_array($dstName)) {
+		$dstName = $this->t($dstName);
+	}
+} else {
+	$dstName = $this->data['dstMetadata']['entityid'];
+}
+
+$attributes = $this->data['attributes'];
+
+
+$this->data['header'] = 'Consent'; /* TODO: translation */
+$this->includeAtTemplateBase('includes/header.php');
+?>
+<div id="content">
+
+<p>
+<?php echo $this->t('{consent:consent_notice}'); ?> <strong><?php echo htmlspecialchars($dstName); ?></strong>.
+<?php echo $this->t('{consent:consent_accept}', array('IDPNAME' => htmlspecialchars($srcName))) ?>
+</p>
+
+<?php
+if ($this->data['sppp'] !== FALSE) {
+	echo "<p>" . htmlspecialchars($this->t('consent_privacypolicy')) . " ";
+	echo "<a target='_new_window' href='" . htmlspecialchars($this->data['sppp']) . "'>" . htmlspecialchars($dstName) . "</a>";
+	echo "</p>";
+}
+?>
+
+<form style="display: inline" action="<?php echo htmlspecialchars($this->data['yesTarget']); ?>">
+	<input type="submit" name="yes" id="yesbutton" value="<?php echo $this->t('{consent:yes}') ?>" />
+<?php
+foreach ($this->data['yesData'] as $name => $value) {
+	echo('<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($value) . '" />');
+}
+if ($this->data['usestorage']) {
+	echo('<input type="checkbox" name="saveconsent" value="1" /> ' . $this->t('{consent:remember}'));
+}
+?>
+</form>
+
+<form style="display: inline; margin-left: .5em;" action="<?php echo htmlspecialchars($this->data['noTarget']); ?>" method="GET">
+<?php
+foreach ($this->data['noData'] as $name => $value) {
+	echo('<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($value) . '" />');
+}
+?>
+	<input type="submit" id="nobutton" value="<?php echo htmlspecialchars($this->t('{consent:no}')) ?>" />
+</form>
+
+<p>
+<table style="font-size: x-small">
+<?php
+foreach ($attributes as $name => $value) {
+	$nameTag = '{attributes:attribute_' . strtolower($name) . '}';
+	if ($this->getTag($nameTag) !== NULL) {
+		$name = $this->t($nameTag);
+	}
+
+	if (sizeof($value) > 1) {
+		echo '<tr><td>' . htmlspecialchars($name) . '</td><td><ul>';
+		foreach ($value AS $v) {
+			echo '<li>' . htmlspecialchars($v) . '</li>';
+		}
+		echo '</ul></td></tr>';
+	} else {
+		echo '<tr><td>' . htmlspecialchars($name) . '</td><td>' . htmlspecialchars($value[0]) . '</td></tr>';
+	}
+}
+
+?>
+</table>
+</p>
+
+<?php
+$this->includeAtTemplateBase('includes/footer.php');
+?>
\ No newline at end of file
diff --git a/modules/consent/templates/default/noconsent.php b/modules/consent/templates/default/noconsent.php
new file mode 100644
index 000000000..b865cbef1
--- /dev/null
+++ b/modules/consent/templates/default/noconsent.php
@@ -0,0 +1,21 @@
+<?php
+	$this->data['header'] = $this->t('{consent:noconsent_title}');;
+	$this->data['icon'] = 'bomb_l.png';
+	$this->includeAtTemplateBase('includes/header.php');
+?>
+
+
+<div id="content">
+
+	<h2><?php echo($this->data['header']); ?></h2>
+	<p><?php echo($this->t('{consent:noconsent_text}')); ?></p>
+
+<?php
+	if($this->data['resumeFrom']) {
+		echo('<p><a href="' . htmlspecialchars($this->data['resumeFrom']) . '">');
+		echo($this->t('{consent:noconsent_return}'));
+		echo('</a></p>');
+	}
+?>
+
+<?php $this->includeAtTemplateBase('includes/footer.php'); ?>
\ No newline at end of file
diff --git a/modules/consent/www/getconsent.php b/modules/consent/www/getconsent.php
new file mode 100644
index 000000000..284f904fc
--- /dev/null
+++ b/modules/consent/www/getconsent.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * This script displays a page to the user, which requests that the user
+ * authorizes the release of attributes.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+
+if (!array_key_exists('StateId', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing required StateId query parameter.');
+}
+
+$id = $_REQUEST['StateId'];
+$state = SimpleSAML_Auth_State::loadState($id, 'consent:request');
+
+
+if (array_key_exists('yes', $_REQUEST)) {
+	/* The user has pressed the yes-button. */
+
+	if (array_key_exists('consent:store', $state) && array_key_exists('saveconsent', $_REQUEST)
+		&& $_REQUEST['saveconsent'] === '1') {
+
+		/* Save consent. */
+		$store = $state['consent:store'];
+		$userId = $state['consent:store.userId'];
+		$destination = $state['consent:store.destination'];
+		$attributeSet = $state['consent:store.attributeSet'];
+		$store->saveConsent($userId, $destination, $attributeSet);
+	}
+
+	SimpleSAML_Auth_ProcessingChain::resumeProcessing($state);
+}
+
+
+/* Show consent form. */
+
+$globalConfig = SimpleSAML_Configuration::getInstance();
+$t = new SimpleSAML_XHTML_Template($globalConfig, 'consent:consentform.php');
+$t->data['srcMetadata'] = $state['Source'];
+$t->data['dstMetadata'] = $state['Destination'];
+$t->data['yesTarget'] = SimpleSAML_Module::getModuleURL('consent/getconsent.php');
+$t->data['yesData'] = array('StateId' => $id);
+$t->data['noTarget'] = SimpleSAML_Module::getModuleURL('consent/noconsent.php');
+$t->data['noData'] = array('StateId' => $id);
+$t->data['attributes'] = $state['Attributes'];
+
+if (array_key_exists('privacypolicy', $state['Destination'])) {
+	$privacypolicy = $state['Destination']['privacypolicy'];
+} elseif (array_key_exists('privacypolicy', $state['Source'])) {
+	$privacypolicy = $state['Source']['privacypolicy'];
+} else {
+	$privacypolicy = FALSE;
+}
+if($privacypolicy !== FALSE) {
+	$privacypolicy = str_replace('%SPENTITYID%', urlencode($spentityid),
+		$privacypolicy);
+}
+$t->data['sppp'] = $privacypolicy;
+
+switch ($state['consent:focus']) {
+case NULL:
+	break;
+case 'yes':
+	$t->data['autofocus'] = 'yesbutton';
+	break;
+case 'no':
+	$t->data['autofocus'] = 'nobutton';
+	break;
+}
+
+if (array_key_exists('consent:store', $state)) {
+	$t->data['usestorage'] = TRUE;
+} else {
+	$t->data['usestorage'] = FALSE;
+}
+
+$t->show();
+exit;
+
+?>
\ No newline at end of file
diff --git a/modules/consent/www/noconsent.php b/modules/consent/www/noconsent.php
new file mode 100644
index 000000000..c84e50606
--- /dev/null
+++ b/modules/consent/www/noconsent.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * This is the page the user lands on when choosing "no" in the consent form.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+
+if (!array_key_exists('StateId', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing required StateId query parameter.');
+}
+
+$id = $_REQUEST['StateId'];
+$state = SimpleSAML_Auth_State::loadState($id, 'consent:request');
+
+$resumeFrom = SimpleSAML_Module::getModuleURL('consent/getconsent.php');
+$resumeFrom = SimpleSAML_Utilities::addURLParameter($resumeFrom, array('StateId' => $id));
+
+$globalConfig = SimpleSAML_Configuration::getInstance();
+
+$t = new SimpleSAML_XHTML_Template($globalConfig, 'consent:noconsent.php');
+$t->data['dstMetadata'] = $state['Destination'];
+$t->data['resumeFrom'] = $resumeFrom;
+$t->show();
+
+?>
\ No newline at end of file
-- 
GitLab