diff --git a/config-templates/authsources.php b/config-templates/authsources.php
index 85aec95bbb515963e3e98b77f1e51775e8544882..1ff4df4918ba3fd29219816db93f2b792b9f1dc0 100644
--- a/config-templates/authsources.php
+++ b/config-templates/authsources.php
@@ -2,6 +2,14 @@
 
 $config = array(
 
+	'example-sql' => array(
+		'sqlauth:SQL',
+		'dsn' => 'pgsql:host=sql.example.org;port=5432;dbname=simplesaml',
+		'username' => 'simplesaml',
+		'password' => 'secretpassword',
+		'query' => 'SELECT "username", "name", "email" FROM "users" WHERE "username" = :username AND "password" = :password',
+	),
+
 	'example-static' => array(
 		'exampleauth:Static',
 		'uid' => 'testuser',
diff --git a/modules/sqlauth/default-enable b/modules/sqlauth/default-enable
new file mode 100644
index 0000000000000000000000000000000000000000..25615cb47c350d23033eb9801627ed8330bcc3e9
--- /dev/null
+++ b/modules/sqlauth/default-enable
@@ -0,0 +1,3 @@
+This file indicates that the default state of this module
+is enabled. To disable, create a file named disable in the
+same directory as this file.
diff --git a/modules/sqlauth/lib/Auth/Source/SQL.php b/modules/sqlauth/lib/Auth/Source/SQL.php
new file mode 100644
index 0000000000000000000000000000000000000000..36ffb77bcdf6b7b4600bbea5f40dff8e86f040fc
--- /dev/null
+++ b/modules/sqlauth/lib/Auth/Source/SQL.php
@@ -0,0 +1,222 @@
+<?php
+
+/**
+ * Simple SQL authentication source
+ *
+ * This class is an example authentication source which authenticates an user
+ * against a SQL database.
+ *
+ * The following options are required:
+ * 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.
+ * - query: The SQL query which should be used to retrieve the user. The parameters :username
+ *          and :password are available. If the username/password is incorrect, the query should
+ *          return no rows. The name of the columns in resultset will be used as attribute names.
+ *          If the query returns multiple rows, they will be merged into the attributes. Duplicate
+ *          values and NULL values will be removed.
+ *
+ * Database layout used in examples:
+ * CREATE TABLE users (
+ *   username VARCHAR(30) NOT NULL PRIMARY KEY,
+ *   password TEXT NOT NULL,
+ *   name TEXT NOT NULL,
+ *   email TEXT NOT NULL
+ * );
+ * CREATE TABLE usergroups (
+ *   username TEXT REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE,
+ *   groupname TEXT,
+ *   UNIQUE(username, groupname)
+ * );
+ *
+ * Example - simple setup, PostgreSQL server:
+ * 'sql-exampleorg' => array(
+ *   'sqlauth:SQL',
+ *   'dsn' => 'pgsql:host=sql.example.org;port=5432;dbname=simplesaml',
+ *   'username' => 'userdb',
+ *   'password' => 'secretpassword',
+ *   'query' => 'SELECT username, name, email FROM users WHERE username = :username AND password = :password',
+ * ),
+ *
+ * Example - multiple groups, MySQL server:
+ * 'sql-exampleorg-groups' => array(
+ *   'sqlauth:SQL',
+ *   'dsn' => 'mysql:host=sql.example.org;dbname=simplesaml',
+ *   'username' => 'userdb',
+ *   'password' => 'secretpassword',
+ *   'query' => 'SELECT users.username, name, email, groupname AS groups FROM users LEFT JOIN usergroups ON users.username=usergroups.username WHERE users.username = :username AND password = :password',
+ * ),
+ *
+ * Example query - MD5 of salt + password, stored as salt + md5(salt + password) in password-field, MySQL server:
+ * SELECT username, name, email
+ * FROM users
+ * WHERE username = :username AND SUBSTRING(password, -32) = MD5(CONCAT(SUBSTRING(password, 1, LENGTH(password) - 32), :password))
+ *
+ * Example query - MD5 of salt + password, stored as salt + md5(salt + password) in password-field, PostgreSQL server:
+ * SELECT username, name, email
+ * FROM users
+ * WHERE username = :username AND SUBSTRING(password FROM LENGTH(password) - 31) = MD5(SUBSTRING(password FROM 1 FOR LENGTH(password) - 32) || :password)
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_sqlauth_Auth_Source_SQL extends sspmod_core_Auth_UserPassBase {
+
+
+	/**
+	 * The DSN we should connect to.
+	 */
+	private $dsn;
+
+
+	/**
+	 * The username we should connect to the database with.
+	 */
+	private $username;
+
+
+	/**
+	 * The password we should connect to the database with.
+	 */
+	private $password;
+
+
+	/**
+	 * The query we should use to retrieve the attributes for the user.
+	 *
+	 * The username and password will be available as :username and :password.
+	 */
+	private $query;
+
+
+	/**
+	 * Constructor for this authentication source.
+	 *
+	 * @param array $info  Information about this authentication source.
+	 * @param array $config  Configuration.
+	 */
+	public function __construct($info, $config) {
+		assert('is_array($info)');
+		assert('is_array($config)');
+
+		/* Call the parent constructor first, as required by the interface. */
+		parent::__construct($info, $config);
+
+		/* Make sure that all required parameters are present. */
+		foreach (array('dsn', 'username', 'password', 'query') as $param) {
+			if (!array_key_exists($param, $config)) {
+				throw new Exception('Missing required attribute \'' . $param .
+					'\' for authentication source ' . $this->authId);
+			}
+
+			if (!is_string($config[$param])) {
+				throw new Exception('Expected parameter \'' . $param .
+					'\' for authentication source ' . $this->authId .
+					' to be a string. Instead it was: ' .
+					var_export($config[$param], TRUE));
+			}
+		}
+
+		$this->dsn = $config['dsn'];
+		$this->username = $config['username'];
+		$this->password = $config['password'];
+		$this->query = $config['query'];
+	}
+
+
+	/**
+	 * Attempt to log in using the given username and password.
+	 *
+	 * On a successful login, this function should return the users attributes. On failure,
+	 * it should throw an exception. If the error was caused by the user entering the wrong
+	 * username or password, a SimpleSAML_Error_Error('WRONGUSERPASS') should be thrown.
+	 *
+	 * Note that both the username and the password are UTF-8 encoded.
+	 *
+	 * @param string $username  The username the user wrote.
+	 * @param string $password  The password the user wrote.
+	 * @return array  Associative array with the users attributes.
+	 */
+	protected function login($username, $password) {
+		assert('is_string($username)');
+		assert('is_string($password)');
+
+		try {
+			$db = new PDO($this->dsn, $this->username, $this->password);
+		} catch (PDOException $e) {
+			throw new Exception('sqlauth:' . $this->authId . ': - Failed to connect to \'' .
+				$this->dsn . '\': '. $e->getMessage());
+		}
+
+		$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+		try {
+			$sth = $db->prepare($this->query);
+		} catch (PDOException $e) {
+			throw new Exception('sqlauth:' . $this->authId .
+				': - Failed to prepare query: ' . $e->getMessage());
+		}
+
+		try {
+			$res = $sth->execute(array('username' => $username, 'password' => $password));
+		} catch (PDOException $e) {
+			throw new Exception('sqlauth:' . $this->authId .
+				': - Failed to execute query: ' . $e->getMessage());
+		}
+
+		try {
+			$data = $sth->fetchAll(PDO::FETCH_ASSOC);
+		} catch (PDOException $e) {
+			throw new Exception('sqlauth:' . $this->authId .
+				': - Failed to fetch result set: ' . $e->getMessage());
+		}
+
+		SimpleSAML_Logger::info('sqlauth:' . $this->authId . ': Got ' . count($data) .
+			' rows from database');
+
+		if (count($data) === 0) {
+			/* No rows returned - invalid username/password. */
+			SimpleSAML_Logger::error('sqlauth:' . $this->authId .
+				': No rows in result set. Probably wrong username/password.');
+			throw new SimpleSAML_Error_Error('WRONGUSERPASS');
+		}
+
+		/* Extract attributes. We allow the resultset to consist of multiple rows. Attributes
+		 * which are present in more than one row will become multivalued. NULL values and
+		 * duplicate values will be skipped. All values will be converted to strings.
+		 */
+		$attributes = array();
+		foreach ($data as $row) {
+			foreach ($row as $name => $value) {
+
+				if ($value === NULL) {
+					continue;
+				}
+
+				$value = (string)$value;
+
+				if (!array_key_exists($name, $attributes)) {
+					$attributes[$name] = array();
+				}
+
+				if (in_array($value, $attributes[$name], TRUE)) {
+					/* Value already exists in attribute. */
+					continue;
+				}
+
+				$attributes[$name][] = $value;
+			}
+		}
+
+		SimpleSAML_Logger::info('sqlauth:' . $this->authId . ': Attributes: ' .
+			implode(',', array_keys($attributes)));
+
+		return $attributes;
+	}
+
+}
+
+?>
\ No newline at end of file