diff --git a/modules/saml/docs/nameid.txt b/modules/saml/docs/nameid.txt index 1fa3c63abfc9cbf6d365fcca30e060b6fd31e110..3feb648f25d898b9588ebc212685e9c817c56c8e 100644 --- a/modules/saml/docs/nameid.txt +++ b/modules/saml/docs/nameid.txt @@ -60,6 +60,23 @@ Generates a transient NameID with the format `urn:oasis:names:tc:SAML:2.0:nameid No extra options are available for this filter. +`saml:SQLPersistentNameID` +-------------------------- + +Generates and stores persistent NameIDs in a SQL datastore. + +This filter generates and stores a persistent NameID in a SQL datastore. +To use this filter, simpleSAMLphp must be configured to use a SQL datastore. +See the `store.type` configuration option in `config.php`. + +This filter will only create new NameIDs when the SP specifies `AllowCreate="true"` in the authentication request. + +### Options + +`attribute` +: The name of the attribute we should use as the unique user ID. + + Example ------- @@ -79,3 +96,15 @@ This example makes three NameIDs available: 'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', ), ), + +Storing persistent NameIDs in a SQL database: + + 'authproc' => array( + 1 => array( + 'class' => 'saml:TransientNameID', + ), + 2 => array( + 'class' => 'saml:SQLPersistentNameID', + 'attribute' => 'eduPersonPrincipalName', + ), + ), diff --git a/modules/saml/lib/Auth/Process/SQLPersistentNameID.php b/modules/saml/lib/Auth/Process/SQLPersistentNameID.php new file mode 100644 index 0000000000000000000000000000000000000000..ac7534126050e71d1ab57d6208715571fe4e96f3 --- /dev/null +++ b/modules/saml/lib/Auth/Process/SQLPersistentNameID.php @@ -0,0 +1,92 @@ +<?php + +/** + * Authproc filter to generate a persistent NameID. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_saml_Auth_Process_SQLPersistentNameID extends sspmod_saml_BaseNameIDGenerator { + + /** + * Which attribute contains the unique identifier of the user. + * + * @var string + */ + private $attribute; + + + /** + * Initialize this filter, parse 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)'); + + $this->format = SAML2_Const::NAMEID_PERSISTENT; + + if (!isset($config['attribute'])) { + throw new SimpleSAML_Error_Exception('PersistentNameID: Missing required option \'attribute\'.'); + } + $this->attribute = $config['attribute']; + } + + + /** + * Get the NameID value. + * + * @return string|NULL The NameID value. + */ + protected function getValue(array &$state) { + + if (!isset($state['saml:NameIDFormat']) || $state['saml:NameIDFormat'] !== $this->format) { + SimpleSAML_Logger::debug('SQLPersistentNameID: Request did not specify persistent NameID format - not generating persistent NameID.'); + return NULL; + } + + if (!isset($state['Destination']['entityid'])) { + SimpleSAML_Logger::warning('SQLPersistentNameID: No SP entity ID - not generating persistent NameID.'); + return NULL; + } + $spEntityId = $state['Destination']['entityid']; + + if (!isset($state['Source']['entityid'])) { + SimpleSAML_Logger::warning('SQLPersistentNameID: No IdP entity ID - not generating persistent NameID.'); + return NULL; + } + $idpEntityId = $state['Source']['entityid']; + + if (!isset($state['Attributes'][$this->attribute]) || count($state['Attributes'][$this->attribute]) === 0) { + SimpleSAML_Logger::warning('SQLPersistentNameID: Missing attribute ' . var_export($this->attribute, TRUE) . ' on user - not generating persistent NameID.'); + return NULL; + } + if (count($state['Attributes'][$this->attribute]) > 1) { + SimpleSAML_Logger::warning('SQLPersistentNameID: More than one value in attribute ' . var_export($this->attribute, TRUE) . ' on user - not generating persistent NameID.'); + return NULL; + } + $uid = array_values($state['Attributes'][$this->attribute]); /* Just in case the first index is no longer 0. */ + $uid = $uid[0]; + + + $value = sspmod_saml_IdP_SQLNameID::get($idpEntityId, $spEntityId, $uid); + if ($value !== NULL) { + SimpleSAML_Logger::debug('SQLPersistentNameID: Found persistent NameID ' . var_export($value, TRUE) . ' for user ' . var_export($uid, TRUE) . '.'); + return $value; + } + + if (!isset($state['saml:AllowCreate']) || !$state['saml:AllowCreate']) { + SimpleSAML_Logger::warning('SQLPersistentNameID: Did not find persistent NameID for user, and not allowed to create new NameID.'); + throw new sspmod_saml_Error(SAML2_Const::STATUS_RESPONDER, 'urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy'); + } + + $value = SimpleSAML_Utilities::stringToHex(SimpleSAML_Utilities::generateRandomBytes(20)); + SimpleSAML_Logger::debug('SQLPersistentNameID: Created persistent NameID ' . var_export($value, TRUE) . ' for user ' . var_export($uid, TRUE) . '.'); + sspmod_saml_IdP_SQLNameID::add($idpEntityId, $spEntityId, $uid, $value); + + return $value; + } + +} diff --git a/modules/saml/lib/IdP/SQLNameID.php b/modules/saml/lib/IdP/SQLNameID.php new file mode 100644 index 0000000000000000000000000000000000000000..9560765897e4f7195dd96577cb385dda7a72ef79 --- /dev/null +++ b/modules/saml/lib/IdP/SQLNameID.php @@ -0,0 +1,172 @@ +<?php + +/** + * Helper class for working with persistent NameIDs stored in SQL datastore. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_saml_IdP_SQLNameID { + + /** + * Create NameID table in SQL, if it is missing. + * + * @param SimpleSAML_Store_SQL $store The datastore. + */ + private static function createTable(SimpleSAML_Store_SQL $store) { + + if ($store->getTableVersion('saml_PersistentNameID') === 1) { + return; + } + + $query = 'CREATE TABLE ' . $store->prefix . '_saml_PersistentNameID ( + _idp VARCHAR(256) NOT NULL, + _sp VARCHAR(256) NOT NULL, + _user VARCHAR(256) NOT NULL, + _value VARCHAR(40) NOT NULL, + UNIQUE (_idp, _sp, _user) + )'; + $store->pdo->exec($query); + + $query = 'CREATE INDEX ' . $store->prefix . '_saml_PersistentNameID_idp_sp ON ' . $store->prefix . '_saml_PersistentNameID (_idp, _sp)'; + $store->pdo->exec($query); + + $store->setTableVersion('saml_PersistentNameID', 1); + } + + + /** + * Retrieve the SQL datastore. + * + * Will also ensure that the NameID table is present. + * + * @return SimpleSAML_Store_SQL SQL datastore. + */ + private static function getStore() { + + $store = SimpleSAML_Store::getInstance(); + if (!($store instanceof SimpleSAML_Store_SQL)) { + throw new SimpleSAML_Error_Exception('SQL NameID store requires simpleSAMLphp to be configured with a SQL datastore.'); + } + + self::createTable($store); + + return $store; + } + + + /** + * Add a NameID into the database. + * + * @param SimpleSAML_Store_SQL $store The data store. + * @param string $idpEntityId The IdP entityID. + * @param string $spEntityId The SP entityID. + * @param string $user The user's unique identificator (e.g. username). + * @param string $value The NameID value. + */ + public static function add($idpEntityId, $spEntityId, $user, $value) { + assert('is_string($idpEntityId)'); + assert('is_string($spEntityId)'); + assert('is_string($user)'); + assert('is_string($value)'); + + $store = self::getStore(); + + $params = array( + '_idp' => $idpEntityId, + '_sp' => $spEntityId, + '_user' => $user, + '_value' => $value, + ); + + $query = 'INSERT INTO ' . $store->prefix . '_saml_PersistentNameID (_idp, _sp, _user, _value) VALUES(:_idp, :_sp, :_user, :_value)'; + $query = $store->pdo->prepare($query); + $query->execute($params); + } + + + /** + * Retrieve a NameID into from database. + * + * @param string $idpEntityId The IdP entityID. + * @param string $spEntityId The SP entityID. + * @param string $user The user's unique identificator (e.g. username). + * @return string|NULL $value The NameID value, or NULL of no NameID value was found. + */ + public static function get($idpEntityId, $spEntityId, $user) { + assert('is_string($idpEntityId)'); + assert('is_string($spEntityId)'); + assert('is_string($user)'); + + $store = self::getStore(); + + $params = array( + '_idp' => $idpEntityId, + '_sp' => $spEntityId, + '_user' => $user, + ); + + $query = 'SELECT _value FROM ' . $store->prefix . '_saml_PersistentNameID WHERE _idp = :_idp AND _sp = :_sp AND _user = :_user'; + $query = $store->pdo->prepare($query); + $query->execute($params); + + $row = $query->fetch(PDO::FETCH_ASSOC); + if ($row === FALSE) { + /* No NameID found. */ + return NULL; + } + + return $row['_value']; + } + + + /** + * Delete a NameID from the database. + * + * @param string $idpEntityId The IdP entityID. + * @param string $spEntityId The SP entityID. + * @param string $user The user's unique identificator (e.g. username). + */ + public static function delete($idpEntityId, $spEntityId, $user) { + assert('is_string($idpEntityId)'); + assert('is_string($spEntityId)'); + assert('is_string($user)'); + + $store = self::getStore(); + + $params = array( + '_idp' => $idpEntityId, + '_sp' => $spEntityId, + '_user' => $user, + ); + + $query = 'DELETE FROM ' . $store->prefix . '_saml_PersistentNameID WHERE _idp = :_idp AND _sp = :_sp AND _user = :_user'; + $query = $store->pdo->prepare($query); + $query->execute($params); + } + + + /** + * Retrieve all federated identities for an IdP-SP pair. + * + * @param string $idpEntityId The IdP entityID. + * @param string $spEntityId The SP entityID. + * @return array Array of userid => NameID. + */ + public static function getIdentities($idpEntityId, $spEntityId) { + assert('is_string($idpEntityId)'); + assert('is_string($spEntityId)'); + + $query = 'SELECT _user, _value FROM ' . $store->prefix . '_saml_PersistentNameID WHERE _idp = :_idp AND _sp = :_sp'; + $query = $store->pdo->prepare($query); + $query->execute($params); + + $res = array(); + while ( ($row = $query->fetch(PDO::FETCH_ASSOC)) !== FALSE) { + $res[$row['_user']] = $row['_value']; + } + + return $res; + } + +}