User authentication in Drupal 8 - passwords

We saw how general authentication works with Drupal 8 in the previous post. We shall see how the actual authentication happens when user logs in. It all begins with a humble login route in user.services.yml of the user module.

user.login:
  path: '/user/login'
  defaults:
    _form: '\Drupal\user\Form\UserLoginForm'
    _title: 'Log in'
  requirements:
    _user_is_logged_in: 'FALSE'
  options:
    _maintenance_access: TRUE

The login form takes the username, password, validates them both, applies flood control policy and logs in the user finally if everything checks in. The username and password validation is delegated to a user.auth service implemented in the user module.

user.auth:
  class: Drupal\user\UserAuth
  arguments: ['@entity.manager', '@password']

This service does just one job(like any other service) of checking whether the given password matches the user's password in the records. This happens in the authenticate method of the service.

Detour: how passwords are stored

A secure framework or system does not store your passwords as is. It gets converted into hashes using one-way encryption functions(implying that there is no way to get your original password text from the hash) and is stored in the database.

For example, if your password is Sup3r5ecr3t123!, encrypting it using one of the many hash functions, we get:

$password = 'Sup3r5ecr3t123!';
echo hash('sha512', $password);
// 31441cb4a78d2c8a4708669fc2f1ca60304b79a4c04b22d8b9ae745138e8768a0cda35fc31133a332a7ec438bde679b6cbb818dd987af791e6ad97cf45c6c40b
echo hash('sha512', $password);
// 31441cb4a78d2c8a4708669fc2f1ca60304b79a4c04b22d8b9ae745138e8768a0cda35fc31133a332a7ec438bde679b6cbb818dd987af791e6ad97cf45c6c40b
$password = 'correcthorsebatterystaple';
echo hash('sha512', $password);
// a83e2a21b827b45329811a22c200088598504be804b3348c36d435b3a28f506eb396417721cf5e9da1ac43999ce28098cf507b4dfda9ea3b7b0d885022a51c8a

and here's how it looks in the table:

Plain hash Plain hash

This only reduces one step in the problem, namely, 2 passwords which are the same will have the same hash. So, a hacker can take a predictable password, hash it and do a lookup against the list of hashed passwords. This is the hashed equivalent of a dictionary attack, called Rainbow table.

To avoid this, we use what's called a salted hash. A salt is a random sequence of bytes added to each password before hashing it. This way, it yields different hashes for the same password. This renders the rainbow table technique useless.

$password = 'Sup3r5ecr3t123!';
$hashed1 = password_hash($password, PASSWORD_DEFAULT);
echo $hashed1; // $2y$10$ZjFa5qv5QSbZ6QJnILDRae9qaQz78kOil9IwTYAyQaUmzH5AJcwOe
$hashed2 = password_hash($password, PASSWORD_DEFAULT);
echo $hashed2; // $2y$10$9EeNiz0qjKbuRdR3znj8iutlzMCKcGFzyr78IjRnoUVVk2.ZGmLka
var_dump(password_verify($password, $hashed1)); // bool(true)
var_dump(password_verify($password, $hashed2)); // bool(true)

The password_hash function returns the hashing algorithm, salt and other details as part the hashed output. This information can be used to verify the password using the password_verify function.

Here's how the user table looks now.

Salted hash Salted hash

Password service

The password service in Drupal 8 does these set of functions, including hashing, checking/verification of passwords and salt generation. Let's quickly create 3 new users in Drupal 8, 2 of whom apparently have the same password.

function create_user($username, $password) {
  $user = \Drupal\user\Entity\User::create();
  $user->setPassword($password);
  $user->enforceIsNew();
  $user->setEmail($username . '@example.com');
  $user->setUsername($username);
  $user->activate();
  $result = $user->save();
}


create_user('foo', 'password123');
create_user('bar', 'password123');
create_user('baz', 'Sup3r5ecr3t123!');

Here's how the table looks like now.

Drupal 8 user table Drupal 8 user table

Let's use the password service and verify if the password entered by the user is correct.

$password_hasher = \Drupal::service('password');
$password = 'password123';
// fetched from 'pass' column of 'users_field_data' table for user 'foo'.
$hashed_password = '$S$EoUtgqZ9f9EsZNAmkSXqOPjhZ74S9Wu1Yat58FDdWn59hWYXyMLP'; 
var_dump($password_hasher->check($password, $hashed_password)); // yields true

We can even compare against a computed hash, as in:

$password_hasher = \Drupal::service('password');
$password = 'helloworld123';
$hashed_password = $password_hasher->hash($password);
echo $hashed_password; // $S$EPMNci//I8B1B71ThZEkXxwfxf8ORJowi89uXnGFKxXaQpGI.oSU
var_dump($password_hasher->check($password, $hashed_password));

You can write your own Password hashing and checking mechanism by overriding the Drupal\Core\Password\PasswordInterface, although employ this as a last measure. It is advised not to use any new/home brewed/untested cryptographic methods to deal with user authentication.

Resources

Drupal 8 module development

Like what you read?

Then you will definitely love my new book about Drupal 8 module development.