diff --git a/config-templates/config.php b/config-templates/config.php index a38eff60acdc41ebe0a119d51381446704f5033e..23a2f3a646865e76e0b77301bf23173115bb8f57 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -138,6 +138,16 @@ $config = array ( */ 'idpdisco.layout' => 'links', + /* + * Configuration of Consent storage used for attribute consent. + * connect, user and passwd is used with PDO (in example Mysql) + */ + 'consent_usestorage' => FALSE, + 'consent_userid' => 'eduPersonPrincipalName', + 'consent_salt' => 'sdkfjhsidu87werwe8r79w8e7r', + 'consent_pdo_connect' => 'mysql:host=sql.example.org;dbname=simplesamlconsent', + 'consent_pdo_user' => 'simplesamluser', + 'consent_pdo_passwd' => 'xxxx', /* * This option configures the metadata sources. The metadata sources is given as an array with diff --git a/lib/SimpleSAML/Consent/Consent.php b/lib/SimpleSAML/Consent/Consent.php new file mode 100644 index 0000000000000000000000000000000000000000..eff97f64c296fffbedfbe36e7f0ef82ecf9eb383 --- /dev/null +++ b/lib/SimpleSAML/Consent/Consent.php @@ -0,0 +1,165 @@ +<?php + +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Configuration.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Utilities.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/SessionHandler.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Logger.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Consent/ConsentStorage.php'); + +/** + * The Consent class is used for Attribute Release consent. + * + * @author Mads, Lasse, David, Peter and Andreas. + * @package simpleSAMLphp + * @version $Id$ + */ +class SimpleSAML_Consent_Consent { + + + private $config; + private $session; + private $spentityid; + private $idpentityid; + + private $salt; + + private $attributes; + private $filteredattributes; + + private $storageerror; + + /** + * Constructor + */ + public function __construct($config, $session, $spentityid, $idpentityid, $attributes, $filteredattributes) { + + $this->config = $config; + $this->salt = $this->config->getValue('consent_salt', 'eae46a3d5cb6e8546dded65be9855e5c'); + $this->attributes = $attributes; + $this->filteredattributes = $filteredattributes; + $this->session = $session; + $this->spentityid = $spentityid; + $this->idpentityid = $idpentityid; + + $this->storageerror = false; + } + + /** + * An identifier for the federation (IdP). Will use SAML 2.0 IdP remote if running in bridge + * mode. If running as a standalone IdP, use the hosted IdP entity ID. + * + * @return Identifier of the IdP + */ + private function getIdPID() { + + if ($this->session->getAuthority() === 'saml2') { + return $this->session->getIdP(); + } + + // from the local idp + return $this->idpentityid; + } + + /** + * Generate a globally unique identifier of the user. Will also be anonymous (hashed). + * + * @return hash( eduPersonPrincipalName + salt + IdP-identifier ) + */ + private function getHashedUserID() { + $userid_attributename = $this->config->getValue('consent_userid', 'eduPersonPrincipalName'); + + if (empty($this->attributes[$userid_attributename])) { + throw new Exception('Could not generate useridentifier for storing consent. Attribute [' . + $userid_attributename . '] was not available.'); + } + + $userid = $this->attributes[$userid_attributename]; + + return hash('sha1', $userid . $this->salt . $this->getIdPID() ); + } + + /** + * Get a targeted ID. An identifier that is unique per SP entity ID. + */ + private function getTargetedID($hashed_userid) { + + return hash('sha1', $hashed_userid . $salt . $this->spentityid); + + } + + /** + * Get a hash value that changes when attributes are added or attribute values changed. + */ + private function getAttributeHash() { + return hash('sha1', serialize($this->filteredattributes)); + } + + public function useStorage() { + if ($this->storageerror) return false; + return $this->config->getValue('consent_usestorage', false); + } + + + public function consent() { + + + /** + * The user has manually accepted consent and chosen not to store the consent + * for later. + */ + if (isset($_GET['consent']) && !isset($_GET['saveconsent'])) { + return true; + } + + if (!$this->useStorage() ) { + return false; + } + + /* + * Generate identifiers and hashes + */ + $hashed_user_id = $this->getHashedUserID(); + $targeted_id = $this->getTargetedID($hashed_user_id); + $attribute_hash = $this->getAttributeHash(); + + try { + // Create a consent storage. + $consent_storage = new SimpleSAML_Consent_Storage($this->config); + + } catch (Exception $e ) { + SimpleSAML_Logger::error('Library - Consent: Error connceting to storage: ' . $e->getMessage() ); + $this->storageerror = true; + return false; + } + /** + * User has given cosent and asked for storing it for later. + */ + if (isset($_GET['consent']) && isset($_GET['saveconsent'])) { + try { + $consent_storage->store($hashed_user_id, $targeted_id, $attribute_hash); + } catch (Exception $e) { + SimpleSAML_Logger::error('Library - Consent: Error connceting to storage: ' . $e->getMessage() ); + } + return true; + } + + /** + * Check if consent exists in storage, and if it does update the usage time stamp + * and return true. + */ + try { + if ($consent_storage->lookup($hashed_user_id, $targeted_id, $attribute_hash)) { + SimpleSAML_Logger::notice('Library - Consent consent(): Found stored consent.'); + return true; + } + } catch (Exception $e) { + SimpleSAML_Logger::error('Library - Consent: Error connceting to storage: ' . $e->getMessage() ); + } + + return false; + } + + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/Consent/ConsentStorage.php b/lib/SimpleSAML/Consent/ConsentStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..6ad2097da69c07b59f4b5a60470c11d83f8c6a8e --- /dev/null +++ b/lib/SimpleSAML/Consent/ConsentStorage.php @@ -0,0 +1,87 @@ +<?php + +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Configuration.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Utilities.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/SessionHandler.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Logger.php'); + +/** + * The Consent Storage class is used for storing Attribute Release consents. + * + * CREATE TABLE consent ( + * federation_id varchar(128) NOT NULL, + * service_id varchar(128) NOT NULL, + * attribute varchar(128) NOT NULL, + * consent_date datetime NOT NULL, + * usage_date datetime NOT NULL, + * PRIMARY KEY USING BTREE (federation_id, service_id) + * ); + * + * @author Mads, Lasse, David, Peter and Andreas. + * @package simpleSAMLphp + * @version $Id$ + */ +class SimpleSAML_Consent_Storage { + + private $config; + private $dbh; + + /** + * Constructor + */ + public function __construct($config) { + + $this->config = $config; + + $pdo_connect = $config->getValue('consent_pdo_connect'); + $pdo_user = $config->getValue('consent_pdo_user'); + $pdo_passwd = $config->getValue('consent_pdo_passwd'); + + $this->dbh = new PDO($pdo_connect, $pdo_user, $pdo_passwd, + array( PDO::ATTR_PERSISTENT => false)); + + //$this->dbh->setAttribute('PDO::ATTR_TIMEOUT', 5); + + } + + + /** + * Lookup consent database for an entry, and update the timestamp. + * + * @return Will return true if consent is stored, and false if consent is not stored. + */ + public function lookup($user_id, $targeted_id, $attribute_hash) { + $stmt = $this->dbh->prepare("UPDATE consent SET usage_date = NOW() WHERE federation_id = ? AND service_id = ? AND attribute = ?"); + $stmt->execute(array($user_id, $targeted_id, $attribute_hash)); + $rows = $stmt->rowCount(); + + SimpleSAML_Logger::debug('Library - ConsentStorage get(): user_id : ' . $user_id); + SimpleSAML_Logger::debug('Library - ConsentStorage get(): targeted_id : ' . $targeted_id); + SimpleSAML_Logger::debug('Library - ConsentStorage get(): attribute_hash : ' . $attribute_hash); + + SimpleSAML_Logger::debug('Library - ConsentStorage get(): Number of rows : [' . $rows . ']'); + + return ($rows === 1); + } + + + /** + * Store user consent in database + */ + public function store($user_id, $targeted_id, $attribute_hash) { + + SimpleSAML_Logger::debug('Library - ConsentStorage store(): user_id : ' . $user_id); + SimpleSAML_Logger::debug('Library - ConsentStorage store(): targeted_id : ' . $targeted_id); + SimpleSAML_Logger::debug('Library - ConsentStorage store(): attribute_hash : ' . $attribute_hash); + + /** + * insert new entry into consent storage. + */ + $stmt = $this->dbh->prepare("REPLACE INTO consent VALUES (?,?,?,NOW(),NOW())"); + $stmt->execute(array($user_id, $targeted_id, $attribute_hash)); + + } + +} + +?> \ No newline at end of file diff --git a/templates/default/en/consent.php b/templates/default/en/consent.php index 532063669f30a39629d8672db32a1f33d3894d8d..3ee6e0f613f3a54149cb0e456d257a10e931be9c 100644 --- a/templates/default/en/consent.php +++ b/templates/default/en/consent.php @@ -3,17 +3,38 @@ <div id="content"> - <p>You are about to login to the service <strong><?php echo htmlspecialchars($data['spentityid']); ?></strong>. In the login proccess, the identity provider will send attributes containing information about your identity to this service. Do you accept this?</p> - - <p><a href="<?php echo htmlspecialchars($data['consenturl']); ?>"><strong>Yes</strong>, I accept that attributes are sent to this service</a></p> + <p>You are about to login to the service <strong><?php echo htmlspecialchars($data['sp_name']); ?></strong>. In the login proccess, the identity provider will send attributes containing information about your identity to this service. Do you accept this?</p> - <p style="font-size: x-small">[ <a href="">Show attributes that are sent</a> ]</p> + + + + <form action="<?php echo htmlspecialchars($data['consenturl']); ?>"> + <input type="submit" value="Yes"> + <input type="hidden" name="consent" value="1"> + <input type="hidden" name="RequestID" value="<?php echo $this->data['requestid']; ?>"> + <?php if($this->data['usestorage']) { ?> + <input type="checkbox" name="saveconsent" id="saveconsent" value="1"> remember consent + <?php } ?> + </form> + <form action="<?php echo htmlspecialchars($this->data['noconsent']); ?>" method="GET"> + <input type="submit" value="No"> + </form> + + + + + <table style="font-size: x-small"> <?php $attributes = $data['attributes']; foreach ($attributes AS $name => $value) { + + if (isset($this->data['attribute_' . htmlspecialchars(strtolower($name)) ])) { + $name = $this->data['attribute_' . htmlspecialchars(strtolower($name))]; + } + if (sizeof($value) > 1) { echo '<tr><td>' . htmlspecialchars($name) . '</td><td><ul>'; foreach ($value AS $v) { diff --git a/templates/default/en/noconsent.php b/templates/default/en/noconsent.php new file mode 100644 index 0000000000000000000000000000000000000000..dac22d765925c219d93b51e9904a538b32c0f1a4 --- /dev/null +++ b/templates/default/en/noconsent.php @@ -0,0 +1,16 @@ +<?php + $this->data['header'] = 'No consent was given'; + $this->data['icon'] = 'bomb_l.png'; + $this->includeAtTemplateBase('includes/header.php'); +?> + + +<div id="content"> + + <h2><?php echo $this->data['title']; ?></h2> + + +You did not accept to give consent. + + +<?php $this->includeAtTemplateBase('includes/footer.php'); ?> \ No newline at end of file diff --git a/www/noconsent.php b/www/noconsent.php new file mode 100644 index 0000000000000000000000000000000000000000..a10cf3ff7d72251c2461f5b353dfd3bc04b9f8d8 --- /dev/null +++ b/www/noconsent.php @@ -0,0 +1,14 @@ +<?php + +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . '_include.php'); + +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/XHTML/Template.php'); + +/* Load simpleSAMLphp, configuration */ +$config = SimpleSAML_Configuration::getInstance(); +$session = SimpleSAML_Session::getInstance(true); + +$t = new SimpleSAML_XHTML_Template($config, 'noconsent.php'); +$t->show(); + +?> \ No newline at end of file diff --git a/www/saml2/idp/SSOService.php b/www/saml2/idp/SSOService.php index d2da32f26df7042f90acf5d03028aa7284f5e164..3296d0f5286a13697d0f5046d85abe9f2172ca5f 100644 --- a/www/saml2/idp/SSOService.php +++ b/www/saml2/idp/SSOService.php @@ -12,6 +12,7 @@ require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . '../../../www/_include.php'); require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Utilities.php'); +require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Consent/Consent.php'); require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Session.php'); require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Logger.php'); require_once((isset($SIMPLESAML_INCPREFIX)?$SIMPLESAML_INCPREFIX:'') . 'SimpleSAML/Metadata/MetaDataStorageHandler.php'); @@ -52,7 +53,6 @@ if (!$config->getValue('enable.saml20-idp', false)) */ if (isset($_GET['SAMLRequest'])) { - try { $binding = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); $authnrequest = $binding->decodeRequest($_GET); @@ -107,7 +107,6 @@ if (isset($_GET['SAMLRequest'])) { SimpleSAML_Utilities::fatalError($session->getTrackID(), 'CACHEAUTHNREQUEST', $exception); } - } else { SimpleSAML_Utilities::fatalError($session->getTrackID(), 'SSOSERVICEPARAMS'); } @@ -148,31 +147,7 @@ if (!isset($session) || !$session->isValid($authority) ) { $spentityid = $requestcache['Issuer']; $spmetadata = $metadata->getMetaData($spentityid, 'saml20-sp-remote'); - /* - * Dealing with attribute release consent. - */ - - if (array_key_exists('requireconsent', $idpmetadata) - && $idpmetadata['requireconsent']) { - - if (!isset($_GET['consent'])) { - - SimpleSAML_Logger::info('SAML2.0 - IdP.SSOService: Requires consent from user for attribute release'); - - $t = new SimpleSAML_XHTML_Template($config, 'consent.php'); - $t->data['header'] = 'Consent'; - $t->data['spentityid'] = $spentityid; - $t->data['attributes'] = $session->getAttributes(); - $t->data['consenturl'] = SimpleSAML_Utilities::addURLparameter(SimpleSAML_Utilities::selfURL(), 'consent=1'); - $t->show(); - exit(0); - - } else { - - SimpleSAML_Logger::info('SAML2.0 - IdP.SSOService: Got consent from user'); - } - - } + $sp_name = (isset($spmetadata['name']) ? $spmetadata['name'] : $spentityid); // Adding this service provider to the list of sessions. // Right now the list is used for SAML 2.0 only. @@ -186,7 +161,8 @@ if (!isset($session) || !$session->isValid($authority) ) { /* * Attribute handling */ - $afilter = new SimpleSAML_XML_AttributeFilter($config, $session->getAttributes()); + $attributes = $session->getAttributes(); + $afilter = new SimpleSAML_XML_AttributeFilter($config, $attributes); $afilter->process($idpmetadata, $spmetadata); /** @@ -210,6 +186,45 @@ if (!isset($session) || !$session->isValid($authority) ) { $filteredattributes = $afilter->getAttributes(); + + + /* + * Dealing with attribute release consent. + */ + $requireconsent = false; + if (isset($idpmetadata['requireconsent'])) { + if (is_bool($idpmetadata['requireconsent'])) { + $requireconsent = $idpmetadata['requireconsent']; + } else { + throw new Exception('SAML 2.0 IdP hosted metadata parameter [requireconsent] is in illegal format, must be a PHP boolean type.'); + } + } + if ($requireconsent) { + + $consent = new SimpleSAML_Consent_Consent($config, $session, $spentityid, $idpentityid, $attributes, $filteredattributes); + + if (!$consent->consent()) { + + $t = new SimpleSAML_XHTML_Template($config, 'consent.php', 'attributes.php'); + $t->data['header'] = 'Consent'; + $t->data['sp_name'] = $sp_name; + $t->data['attributes'] = $filteredattributes; + $t->data['consenturl'] = SimpleSAML_Utilities::selfURLNoQuery(); + $t->data['requestid'] = $requestid; + $t->data['usestorage'] = $consent->useStorage(); + $t->data['noconsent'] = '/' . $config->getBaseURL() . 'noconsent.php'; + $t->show(); + exit; + } + + } + // END ATTRIBUTE CONSENT CODE + + + + + + // Generate an SAML 2.0 AuthNResponse message $ar = new SimpleSAML_XML_SAML20_AuthnResponse($config, $metadata); $authnResponseXML = $ar->generate($idpentityid, $spentityid, $requestid, null, $filteredattributes);