Loading...

Sample MySQL Driver

In this guide, we'll create a sample MySQL driver and a table that will contain one user. The example can be applied to any other type of data storage or authentication source.

Creating the table

CREATE TABLE users (
    id int unsigned NOT NULL AUTO_INCREMENT
    username_unique BINARY(20) NOT NULL,
    username VARCHAR(255) NOT NULL,
    password BINARY(60) NOT NULL,
    first_name VARCHAR(255) NOT NULL,
    last_name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    PRIMARY KEY(id),
    UNIQUE(username_unique)
);

Quick explanation of the logic behind this table:

  • We're going to hash the username and save the hash to username_unique column
  • username_unique is made UNIQUE and will be used to look up users
  • password field will contain the value provided by PHP's password_hash function

Adding user to table

INSERT INTO users 
(username_unique, username, password, first_name, last_name, email) 
VALUES 
(UNHEX(SHA1('joe.bloggs'), 'joe.bloggs', 'yRWzoeugNg6w2rYyBub7', 'Joe', 'Bloggs', 'joe@bloggs.com')

Creating driver file skeleton

To create driver file skeleton in directory app/Addons/Drivers, use authstack-ctl driver:new command.

We'll call our driver CustomMySQL.

Command:

$ authstack-ctl driver:new CustomMySQL
Driver created successfully!

Your directory structure will look as per image:

file

Providing driver configuration

For our driver to work, it requires certain configuration values from the connection that we'll create. We'll modify driver.json and provide the schema that will represent required configuration fields.

Required configuration values for driver to work will be:

  • MySQL host (required)
  • MySQL port (optional)
  • MySQL database name (required)
  • MySQL username (required)
  • MySQL password (required)

Also, very important - the configuration must specify which attributes this driver will return upon successful authentication. This is used by AuthStack to perform attribute mapping and attribute retrieval upon successful authentication. Without this, we can't send any user information to Service Providers, so make sure you specify this list of attributes. We will specify the list of attributes under key attributes in our driver.json

Open driver.json file and ensure its contents matches the below:

{
  "title": "Custom MySQL Driver",
  "description": "Sample driver from guide, used for learning purposes",
  "class_path": "AuthStack\\Addons\\Drivers\\CustomMySQL",
  "is_protected": 0,
  "config": {
    "schema": {
      "$schema": "http://json-schema.org/draft-04/schema#",
      "required": ["mysql", "attributes"],
      "properties": {
        "mysql": {
          "type": "object",
          "required": ["host", "database", "username", "password"],
          "properties": {

            "host": {
              "type": "string",
              "description": "MySQL IP or Hostname",
              "default": "127.0.0.1"
            },

            "port": {
              "type": "string",
              "description": "MySQL port.",
              "default": "3306"
            },

            "database": {
              "type": "string",
              "description": "Database name that contains users we want to authenticate"
            },

            "username": {
              "type": "string",
              "description": "MySQL user username"
            },

            "password": {
              "type": "string",
              "description": "MySQL user password"
            }
          }
        },

        "attributes": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["name", "enabled", "value", "renamed", "source_id", "id"],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "value": {
                "type": "string"
              },
              "enabled": {
                "type": "boolean",
                "default": true
              },
              "renamed": {
                "type": "string"
              },
              "source_id": {
                "type": "integer",
                "default": 1
              }
            }
          }
        }
      }
    }
  }
}

With the above schema, we can create connections from AuthStack UI which will use our custom driver for connecting and authenticating.

Change username and password as required.

Adding logic to generated code

Since we're using MySQL, we'll be dealing with PDO for handling database connections. Several methods will use PDO so it makes sense to create a method that creates / retrieves PDO instance. We'll add the following method to Driver class.

We'll create method getPDO() which will return an instance of PDO, if it exists. If not, it throws an Exception since driver hasn't established the connection, yet the method was invoked.

<?php

namespace AuthStack\Addons\Drivers\CustomMySQL;

use AuthStack\Driver\Errors\NoAuthSourceError;
use AuthStack\Driver\Traits\TraitTemplateVariables;

class Driver extends AbstractDriver implements \AuthStack\Driver\Driver\AuthDriverInterface
{
    use TraitTemplateVariables;

    protected function getPDO()
    {
        // @note: Member $pdo is defined in AbstractDriver
        if(!$this->pdo instanceof \PDO)
        {
            throw new NoAuthSourceError("Unable to retrieve PDO instance. Did you connect?");
        }

        return $this->pdo;
    }
}

We can move on to implementing remainder of methods.

Implementing driver methods

connect

Connect method will read configuration variables and create PDO instance. This is where JSON Schema plays a role, since we made it mandatory that configuration is in a certain format. AuthStack will bootstrap the driver with correct configuration format so we will be able to access these variables using $this->config() method.

Our schema specifies a mysql key with properties ["host", "port", "database", "username", "password"], so to access any of those via $this->config() method, simply use dot notation.

Example: $host = $this->config('mysql.host', 'Default value if specified one is not found');


Return values: true on success, false on failure.

Throws \AuthStack\Driver\Errors\ConnectionError if an Exception occurs.

This flow is mandatory, if you omit the exception, AuthStack won't know how to handle Exceptions or error messages about why a connection failed.

<?php

public function connect()
{
    $result = false;

    try
    {
        $dsn = sprintf("mysql:dbname=%s;host=%s;port:%dcharset=UTF8", $this->config('mysql.database'), $this->config("mysql.host"), $this->config('mysql.port', 3306));

        $this->pdo = new \PDO($dsn, $this->config('mysql.username'), $this->config('mysql.password'));

        $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

        $result = true;
    }
    catch(\Exception $e)
    {
        throw new \AuthStack\Driver\Errors\ConnectionError($e->getMessage());
    }

    return $result;
}

authenticate

This method receives an array with user input from frontend SSO form.

<?php

public function authenticate(array $credentials)
{
    // $this->check() method is defined in AbstractDriver. It checks whether $credentials array contains keys 'username' and 'password'.
    $this->check($credentials);

    $result = false;

    try
    {
        // Prepare the statement. Pass the username value, use MySQL's functions to calculate a raw hash
        $stmt = $this->getPDO()->prepare("SELECT * FROM users WHERE username_unique = UNHEX(SHA1(:username))");

        $stmt->bindValue(':username', $credentials['username']);

        $stmt->execute();

        $attributes = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        // There are no records for the query, user doesn't exist in the database
        if(!sizeof($attributes))
        {
            throw new \Exception("No such user in database.");
        }

        // Fetch user attributes from our array. There's only going to be 1 record
        $user = $attributes[0];

        // Verify the password provided. $user['password'] contains password which is hashed via PHP's password_hash
        $valid_password = password_verify($credentials['password'], $user['password']);

        if(!$valid_password)
        {
            // Password was incorrect, throw an \Exception to have it caught a few lines below, with the error message
            throw new \Exception("Invalid credentials");
        }

        // Note - we are going to check if there is NO $credentials['test']
        // Reason for this is unit testing - if an array with key 'test' is supplied, we will NOT provision the user since we don't want to do that during tests

        if(!isset($credentials['test']))
        {
            // retrieveUser method is a custom method that deals with user provisioning and other checks
            $this->retrieveUser($attributes[0]);
        }

        // Authentication was successful, return true
        $result = true;
    }
    catch(\Exception $e)
    {
        $this->last_error = $e->getMessage();
    }

    return $result;
}

retrieveUser

retrieveUser is not enforced by the interface, however this guide will show why and how to implement this method. It's task is to:

  1. Provision the user
  2. Check whether this user has administrative privileges for AuthStack functions
  3. Provide a model that deals with AuthStack's internal user database

This method will be invoked by two other methods, so it makes sense to make it a separate method so we avoid repeating the code.

<?php

/**
* @param array $attributes
*/
protected function retrieveUser(array $attributes)
{
    // Collect the attributes
    $collection = collect($attributes);

    // Initialize user object related to C2MS driver
    $this->user = User::initialize($collection, $this->cfg_key);

    // Set the Primary Key value. $attributes contains 'id' key which is MySQL's auto_increment id. This value can be obtained via User::getPrimaryKeyValue()
    $this->user->setPrimaryKeyValue($attributes['id']);

    // Check if user is admin. This sets the flag internally
    $this->getUser()->checkIfAdmin();

    // Provision user to AuthStack's database, if not exists
    $this->provision($this->user->getUsername());

    // Set the model with provisioned user to User object related to C2MS user authentication
    $this->getUser()->setProvisionedUser($this->getProvisionedUser());

    // AuthStack Id comes from ProvisionedUser model, so we'll set it now
    $this->getUser()->setAuthstackID($this->provisioned->id);

    // Set the authsource so we know which CONNECTION to look up (not driver)
    $this->getUser()->setAuthsourceID($this->getAuthSourceId());

    // Load two factor keys for this user
    $keys = KeyCollection::loadForUser($this->getUser()->getProvisionedUser()->id);

    // Set the key collection for the user
    $this->getUser()->setKeyCollection($keys);
}

passwordChangeAllowed

Indicates whether password changing is allowed if this driver is used. For this guide, we'll allow password change so we can show how to implement password change functionality.

Password is changed using AuthStack profile page.

Returns bool indicating whether change is allowed or not

<?php

public function passwordChangeAllowed()
{
    return true;
}

changePassword

If AuthStack invokes this method, it means it invoked connect and getUserByUsername methods. This lets us use $this->getUser()->getPrimaryKeyValue() method which will let us query the database to obtain our user.

This method receives one argument - an array with 3 keys:

  • password_current
  • password
  • password_confirmation

You should ensure that information in these keys is correct, that current_password matches current user password and that new password is confirmed. You can add additional checks such as password policy rules.


<?php

public function changePassword(array $data)
{
    // Current password wasn't specified
    if(!isset($data['password_current']))
    {
        throw new \InvalidArgumentException("You must specify current password.");
    }

    // New password wasn't specified
    if(!isset($data['password']))
    {
        throw new \InvalidArgumentException("You must specify new password");
    }

    // Simple password policy = must be between 8 and 20 characters.
    if(strlen($data['password']) < 8 || strlen($data['password']) > 20)
    {
        throw new \InvalidArgumentException("Password should be between 8 and 20 characters in length");
    }

    // Ensure that new password was confirmed correctly
    if(0 !== strcmp($data['password'], $data['password_confirmation']))
    {
        throw new \InvalidArgumentException("New password and password confirmation don't match");
    }

    // Load our user
    $model = \AuthStack\Driver\Model\User::findOrFail($this->getUser()->getPrimaryKeyValue());

    // Check if valid old password was entered
    $valid = password_verify($data['old_password'], $model->password);

    // Return false if invalid password was provided
    if(!$valid)
    {
        return false;
    }

    $model->password = password_hash($data['password'], PASSWORD_BCRYPT);

    return $model->save();
}

getUser

getUser method returns an instance of User, found in the same directory as the Driver class. This is a simple method, it should throw an exception if it was invoked and User object isn't there. User object is created and set in retrieveUser method which is invoked when:

  1. User authenticated via SSO
  2. User is authenticated, AuthStack performs auto-authentication
<?php

/**
* @return User|\AuthStack\Driver\Model\User
* @throws NoUserObjectError
*/
public function getUser()
{
    if(!$this->user instanceof User)
    {
        throw new \AuthStack\Driver\Errors\NoUserObjectError('There is no user object available. You must successfully authenticate first.');
    }

    return $this->user;
}

getUserByPrimaryKey

This method is not used internally but exists for potential future use. The idea behind it is that you can use $this->getUser()->getPrimaryKeyValue() method to obtain the authentication source's PK value and then perform a query to find the user.

This isn't always applicable, such as in LDAP's case.

Current approach is to make this method an alias of getUserByUsername.

However, for this guide, we'll implement the method to retrieve our user from the database using primary key.

<?php

/**
 * @param $id
 * @return User|\AuthStack\Driver\Model\User|bool
 */
public function getUserByPrimaryKey($id)
{
    $result = false;

    try
    {
        $stmt = $this->getPDO()->prepare("SELECT * FROM users WHERE id = :id");

        $stmt->bindValue(':id', $id, \PDO::PARAM_INT);

        $stmt->execute();

        $attributes = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        if(!sizeof($attributes))
        {
            throw new \Exception("No such user with id $id.");
        }

        $this->retrieveUser($attributes[0]);

        $result = $this->getUser();
    }
    catch(\Exception $e)
    {
        $this->last_error = $e->getMessage();
    }

    return $result;
}

getUserByUsername

When users are authenticated, their connection and username is saved into a session. This lets AuthStack perform auto-authentication or credential-less authentication.

For this guide, we'll implement this method by looking up the user via their username

<?php

/**
 * @param $username
 * @return User|\AuthStack\Driver\Model\User|bool
 */
public function getUserByPrimaryKey($username)
{
    $result = false;

    try
    {
        $stmt = $this->getPDO()->prepare("SELECT * FROM users WHERE username_unique = UNHEX(SHA1(:username))");

        $stmt->bindValue(':username', $username, \PDO::PARAM_INT);

        $stmt->execute();

        $attributes = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        if(!sizeof($attributes))
        {
            throw new \Exception("No such user with username $username.");
        }

        $this->retrieveUser($attributes[0]);

        $result = $this->getUser();
    }
    catch(\Exception $e)
    {
        $this->last_error = $e->getMessage();
    }

    return $result;
}

Previous Article

Testing a new Driver

Next Article

Overview

We're happy to talk

Our offices are open 8.30am - 7pm GMT, Monday to Friday - but you can always contact us via email. When we receive your email during opening hours, we aim to respond within 30 minutes or less. Should your email reach us out of hours, we will contact you when the office re-opens.

You can contact us using live chat