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:

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. */
],