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;
+	}
+
+}