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