Implementing custom username/password authentication
This is a step-by-step guide for creating a custom username/password authentication source for SimpleSAMLphp. An authentication source is responsible for authenticating the user, typically by getting a username and password, and looking it up in some sort of database.
Create a custom module
All custom code for SimpleSAMLphp should be contained in a
module
.
This ensures that you can upgrade your SimpleSAMLphp installation without overwriting your own code.
In this example, we will call the module
mymodule
.
It will be located under
modules/mymodule
.
First we need to create the module directory:
cd modules
mkdir mymodule
Now that we have our own module, we can move on to creating an authentication source.
Creating a basic authentication source
Authentication sources are implemented using PHP classes.
We are going to create an authentication source named
mymodule:MyAuth
.
It will be implemented in the file
modules/mymodule/src/Auth/Source/MyAuth.php
.
To begin with, we will create a very simple authentication source, where the username and password is hardcoded into the source code.
Create the file
modules/mymodule/src/Auth/Source/MyAuth.php
with the following contents:
<?php
namespace SimpleSAML\Module\mymodule\Auth\Source;
class MyAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
{
protected function login(string $username, string $password): array
{
if ($username !== 'theusername' || $password !== 'thepassword') {
throw new \SimpleSAML\Error\Error(\SimpleSAML\Error\ErrorCodes::WRONGUSERPASS);
}
return [
'uid' => ['theusername'],
'displayName' => ['Some Random User'],
'eduPersonAffiliation' => ['member', 'employee'],
];
}
}
Some things to note:
-
The classname is
\SimpleSAML\Module\mymodule\Auth\Source\MyAuth
. This tells SimpleSAMLphp to look for the class inmodules/mymodule/src/Auth/Source/MyAuth.php
. -
Our authentication source subclasses
\SimpleSAML\Module\core\Auth\UserPassBase
. This is a helper-class that implements much of the common code needed for username/password authentication. -
The
login
function receives the username and password the user enters. It is expected to authenticate the user. If the username or password is correct, it must return a set of attributes for the user. Otherwise, it must throw the\SimpleSAML\Error\Error('WRONGUSERPASS');
exception. -
Attributes are returned as an associative array of
name => values
pairs. All attributes can have multiple values, so the values are always stored in an array.
Configuring our authentication source
Before we can test our authentication source, we must add an entry for it in
config/authsources.php
.
config/authsources.php
contains a list of enabled authentication sources.
The entry looks like this:
'myauthinstance' => [
'mymodule:MyAuth',
],
You can add it to the beginning of the list, so that the file looks something like this:
<?php
$config = [
'myauthinstance' => [
'mymodule:MyAuth',
],
/* Other authentication sources follow. */
];
myauthinstance
is the name of this instance of the authentication source.
(You are allowed to have multiple instances of an authentication source with different configuration.)
The instance name is used to refer to this authentication source in other configuration files.
The first element of the configuration of the authentication source must be
'mymodule:MyAuth'
.
This tells SimpleSAMLphp to look for the
\SimpleSAML\Module\mymodule\Auth\Source\MyAuth
class.
We also need to enable our new module in
config/config.php
.
config/config.php
contains a list of enabled modules.
We want to add our new module to the list of other enabled modules. We can add it to the beginning of the list, so the enabled modules section looks something like this:
'module.enable' => [
'mymodule' => true,
/* Other enabled modules follow. */
],
Testing our authentication source
Now that we have configured the authentication source, we can test it by accessing "authentication"-page of the SimpleSAMLphp web interface.
By default, the web interface can be found on
http://yourhostname.com/simplesaml/
.
(Obviously, "yourhostname.com" should be replaced with your real hostname.)
Then select the "Authentication"-tab, and choose "Test configured authentication sources".
You should then receive a list of authentication sources from
config/authsources.php
.
Select
myauthinstance
, and log in using "theusername" as the username, and "thepassword" as the password.
You should then arrive on a page listing the attributes we return from the
login
function.
Next, you should log out by following the log out link.
Using our authentication source in an IdP
To use our new authentication source in an IdP we just need to update the IdP configuration to use it.
Open
metadata/saml20-idp-hosted.php
.
In that file you should locate the
auth
-option for your IdP, and change it to
myauthinstance
:
<?php
/* ... */
$metadata['https://example.org/saml-idp'] = [
/* ... */
/*
* Authentication source to use. Must be one that is configured in
* 'config/authsources.php'.
*/
'auth' => 'myauthinstance',
/* ... */
];
You can then test logging in to the IdP. If you have logged in previously, you may need to log out first.
Adding configuration to our authentication source
Instead of hardcoding options in our authentication source, they should be configurable.
We are now going to extend our authentication source to allow us to configure the username and password in
config/authsources.php
.
First, we need to define the properties in the class that should hold our configuration:
private $username;
private $password;
Next, we create a constructor for the class. The constructor is responsible for parsing the configuration and storing it in the properties.
public function __construct($info, $config)
{
parent::__construct($info, $config);
if (!is_string($config['username'])) {
throw new Exception('Missing or invalid username option in config.');
}
$this->username = $config['username'];
if (!is_string($config['password'])) {
throw new Exception('Missing or invalid password option in config.');
}
$this->password = $config['password'];
}
We can then use the properties in the
login
function.
The complete class file should look like this:
<?php
class MyAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
{
private $username;
private $password;
public function __construct($info, $config)
{
parent::__construct($info, $config);
if (!is_string($config['username'])) {
throw new Exception('Missing or invalid username option in config.');
}
$this->username = $config['username'];
if (!is_string($config['password'])) {
throw new Exception('Missing or invalid password option in config.');
}
$this->password = $config['password'];
}
protected function login(string $username, string $password): array
{
if ($username !== $this->username || $password !== $this->password) {
throw new \SimpleSAML\Error\Error(\SimpleSAML\Error\ErrorCodes::WRONGUSERPASS);
}
return [
'uid' => [$this->username],
'displayName' => ['Some Random User'],
'eduPersonAffiliation' => ['member', 'employee'],
];
}
}
We can then update our entry in
config/authsources.php
with the configuration options:
'myauthinstance' => [
'mymodule:MyAuth',
'username' => 'theconfigusername',
'password' => 'theconfigpassword',
],
Next, you should go to the "Test configured authentication sources" page again, and test logging in. Note that we have updated the username & password to "theconfigusername" and "theconfigpassword". (You may need to log out first before you can log in again.)
A more complete example - custom database authentication
The sqlauth:SQL authentication source can do simple authentication against SQL databases. However, in some cases it cannot be used, for example because the database layout is too complex, or because the password validation routines cannot be implemented in SQL. What follows is an example of an authentication source that fetches an user from a database, and validates the password using a custom function.
This code assumes that the database contains a table that looks like this:
CREATE TABLE userdb (
username VARCHAR(32) PRIMARY KEY NOT NULL,
password_hash VARCHAR(64) NOT NULL,
full_name TEXT NOT NULL);
An example user (with password "secret"):
INSERT INTO userdb (username, password_hash, full_name)
VALUES('exampleuser', 'QwVYkvlrAMsXIgULyQ/pDDwDI3dF2aJD4XeVxg==', 'Example User');
In this example, the
password_hash
contains a base64 encoded SSHA password.
A SSHA password is created like this:
$password = 'secret';
$numSalt = 8; /* Number of bytes with salt. */
$salt = '';
for ($i = 0; $i < $numSalt; $i++) {
$salt .= chr(mt_rand(0, 255));
}
$digest = sha1($password . $salt, true);
$password_hash = base64_encode($digest . $salt);
The class follows:
<?php
class MyAuth extends \SimpleSAML\Module\core\Auth\UserPassBase
{
/* The database DSN.
* See the documentation for the various database drivers for information about the syntax:
* http://www.php.net/manual/en/pdo.drivers.php
*/
private $dsn;
/* The database username, password & options. */
private $username;
private $password;
private $options;
public function __construct($info, $config)
{
parent::__construct($info, $config);
if (!is_string($config['dsn'])) {
throw new Exception('Missing or invalid dsn option in config.');
}
$this->dsn = $config['dsn'];
if (!is_string($config['username'])) {
throw new Exception('Missing or invalid username option in config.');
}
$this->username = $config['username'];
if (!is_string($config['password'])) {
throw new Exception('Missing or invalid password option in config.');
}
$this->password = $config['password'];
if (isset($config['options']) {
if (!is_array($config['options'])) {
throw new Exception('Missing or invalid options option in config.');
}
$this->options = $config['options'];
}
}
/**
* A helper function for validating a password hash.
*
* In this example we check a SSHA-password, where the database
* contains a base64 encoded byte string, where the first 20 bytes
* from the byte string is the SHA1 sum, and the remaining bytes is
* the salt.
*/
private function checkPassword($passwordHash, $password)
{
$passwordHash = base64_decode($passwordHash, true);
if (empty($passwordHash)) {
throw new \InvalidArgumentException("Password hash is empty or not a valid base64 encoded string.");
}
$digest = substr($passwordHash, 0, 20);
$salt = substr($passwordHash, 20);
$checkDigest = sha1($password . $salt, true);
return $digest === $checkDigest;
}
protected function login(string $username, string $password): array
{
/* Connect to the database. */
$db = new PDO($this->dsn, $this->username, $this->password, $this->options);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
/* Ensure that we are operating with UTF-8 encoding.
* This command is for MySQL. Other databases may need different commands.
*/
$db->exec("SET NAMES 'utf8'");
/* With PDO we use prepared statements. This saves us from having to escape
* the username in the database query.
*/
$st = $db->prepare('SELECT username, password_hash, full_name FROM userdb WHERE username=:username');
if (!$st->execute(['username' => $username])) {
throw new Exception('Failed to query database for user.');
}
/* Retrieve the row from the database. */
$row = $st->fetch(PDO::FETCH_ASSOC);
if (!$row) {
/* User not found. */
SimpleSAML\Logger::warning('MyAuth: Could not find user ' . var_export($username, true) . '.');
throw new \SimpleSAML\Error\Error(\SimpleSAML\Error\ErrorCodes::WRONGUSERPASS);
}
/* Check the password. */
if (!$this->checkPassword($row['password_hash'], $password)) {
/* Invalid password. */
SimpleSAML\Logger::warning('MyAuth: Wrong password for user ' . var_export($username, true) . '.');
throw new \SimpleSAML\Error\Error(\SimpleSAML\Error\ErrorCodes::WRONGUSERPASS);
}
/* Create the attribute array of the user. */
$attributes = [
'uid' => [$username],
'displayName' => [$row['full_name']],
'eduPersonAffiliation' => ['member', 'employee'],
];
/* Return the attributes. */
return $attributes;
}
}
Configured in
config/authsources.php
:
'myauthinstance' => [
'mymodule:MyAuth',
'dsn' => 'mysql:host=sql.example.org;dbname=userdatabase',
'username' => 'db_username',
'password' => 'secret_db_password',
],
And enabled in
config/config.php
:
'module.enable' => [
'mymodule' => true,
/* Other enabled modules follow. */
],