Migrating users - advanced password examples

Last updated on
5 January 2021

Migrating portable Phpass password hashes

Phpass is a portable public domain password hashing framework for use in PHP applications. Phpass has been integrated into the following systems:

  • WordPress 2.5+
  • bbPress
  • Vanilla
  • PivotX 2.1.0+
  • Textpattern 4.4.0+
  • concrete5 5.6.3+
  • phpBB3
  • Joomla starting with versions 2.5.18 and 3.2.1

The password hashes generated with Phpass look like this: 
$P$B4J4RkvSe3QowfF/v6oHionn8CyW.a.

The $P$ is the so called hash type identifier which indicates that the hash is a portable Phpass hash. The string after that consists of salt and a hash. The portable Phpass hashes can be migrated to Drupal 8 as-is. When the user logs in to your Drupal 8 site for the first time, Drupal will re-hash the password. The salted hash used in this example is for a password 'test'.

id: custom_user_migration
label: Custom user migration
source:
  plugin: embedded_data
  data_rows:
    -
      user_id: 1
      name: johnsmith
      mail: johnsmith@example.com
      pw_hash: '$P$B4J4RkvSe3QowfF/v6oHionn8CyW.a.'
      status: 1
  ids:
    user_id:
      type: integer
process:
  name: name
  mail: mail
  pass: pw_hash
  status: status
destination:
  plugin: entity:user

Migrating password hashes generated with crypt() or password_hash()

PHP supports a wide variety of hashing algorithms with crypt() or password_hash(). For example, a password 'test' hashed with Blowfish algorithm could result into the following salted hash: $2y$10$7OH7jh2tEXommZFO9GQMze7h94Py3n.RcjWM/0eG8xslwwDwXip/S

If the source system was a PHP based application that used crypt() or password_hash(), these hashes should work as-is and they can be migrated 1:1 like in the Phpass example above. When the user logs in to your Drupal 8 site for the first time, Drupal will re-hash the password.

Preserving passwords hashed with another algorithm

If the source system passwords are hashed with some other algorithm, it is possible to add support for these hashes by extending the Drupal core password service.

When the user tries to log in, the password hash checking is done in PhpassHashedPassword::check(). As you can see, there is handling for different hash types which are identified by hash type identifier such as U$, $S$, $P$ and $H$.

You can add handling for your legacy hashes provided that know the algorithm that was used to generate the hashes in your source system. You will need to prefix the old hashes with a special hash type identifier. We use a $OLD$ in this example.

This naive example assumes that the source system used the ROT13 algorithm. The string 'grfg' is the encrypted value of 'test'.

id: custom_user_migration_6
label: Custom user migration 6
source:
  plugin: embedded_data
  data_rows:
    -
      user_id: 1
      name: johnsmith
      mail: johnsmith@example.com
      old_hash: 'grfg'
      status: 1
  constants:
    hash_type_prefix: '$OLD$'
  ids:
    user_id:
      type: integer
process:
  name: name
  mail: mail
  pass:
    plugin: concat
    source:
      - constants/hash_type_prefix
      - old_hash
  status: status
destination:
  plugin: entity:user

Create a custom module which provides a custom password service

Write a custom module. The module has human-readable name "My Module", machine name "my_module", and class prefix 'MyModule' in this example. Register your own password service in my_module.services.yml. The argument 16 is the argument for hash stretching iterations used in Drupal 8.

services:
  password:
    class: Drupal\my_module\MyModulePasswordService
    arguments: [16]

Then, let’s create our custom password service class MyModulePasswordService. This needs to be located in my_module/src/MyModulePasswordService.php.

<?php
namespace Drupal\my_module;
 
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Password\PhpassHashedPassword;
use Drupal\Core\Password\PasswordInterface;
 
/**
 * Provides custom password hash comparison for migrated passwords.
 */
class MyModulePasswordService extends PhpassHashedPassword implements PasswordInterface {
 
  /**
   * Overrides PhpassHashedPassword::check() for custom hash comparison.
   *
   * This naive example assumes that the source system used ROT13 algorithm for
   * encrypting passwords. These passwords were migrated with a hash type prefix
   * '$OLD$' to the Drupal 8 database. When user tries to log in, we check if
   * the hash has this prefix and use the same hash comparison algorithm that
   * the source system did.
   *
   * If the password does not have the '$OLD$' prefix, we let the original
   * PhpassHashedPassword::check() method do the hash comparison.
   */
  public function check ($password, $hash) {
    // Check if the hash has a '$OLD$' prefix.
    if (substr($hash, 0, 5) == '$OLD$') {
      // Remove the prefix so that we can compare the hash without it.
      $stored_hash = substr($hash, 5);
      // Compute the hash with the same algorithm as the legacy system did.
      $computed_hash = str_rot13($password);
      // Compare using hashEquals() instead of === to mitigate timing attacks.
      return $computed_hash && Crypt::hashEquals($stored_hash, $computed_hash);
    }
 
    // Hash did not have '$OLD' prefix. Let the parent class do the checking.
    return parent::check($password, $hash);
  }
}

Further reading on password hashes

Help improve this page

Page status: No known problems

You can: