Verified Commit 38187dd0 authored by Dave Long's avatar Dave Long
Browse files

feat: #3581966 [11.x] Introduce kernel parameters for password hashing algorithm and options

By: znerol
By: benjifisher
parent b22f57f5
Loading
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -248,3 +248,9 @@ parameters:
    # This is not applicable when a queue is suspended but does not specify
    # how long to wait before attempting to resume.
    suspendMaximumWait: 30

  # Can be argon2i, argon2id or 2y (bcrypt). Setting to NULL (~) will use PASSWORD_DEFAULT.
  # See https://www.php.net/password_hash
  password.algorithm: ~
  # Options passed to password_hash. See https://www.php.net/password_hash
  password.options: [ ]
+6 −0
Original line number Diff line number Diff line
@@ -78,6 +78,11 @@ parameters:
      - json
    theme:
      - json
  # Can be argon2i, argon2id or 2y (bcrypt). Setting to NULL (~) will use PASSWORD_DEFAULT.
  # See https://www.php.net/password_hash
  password.algorithm: ~
  # Options passed to password_hash. See https://www.php.net/password_hash
  password.options: []
services:
  _defaults:
    autoconfigure: true
@@ -1209,6 +1214,7 @@ services:
  Drupal\Core\Path\PathValidatorInterface: '@path.validator'
  password:
    class: Drupal\Core\Password\PhpPassword
    arguments: ['%password.algorithm%', '%password.options%']
  Drupal\Core\Password\PasswordInterface: '@password'
  password_generator:
    class: Drupal\Core\Password\DefaultPasswordGenerator
+6 −2
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ class PhpPassword implements PasswordInterface {
  /**
   * Constructs a new password hashing instance.
   *
   * @param string $algorithm
   * @param string|null $algorithm
   *   The hashing algorithm to use. Defaults to PHP default.
   * @param array $options
   *   List of options. Refer to password_hash() for available options.
@@ -20,9 +20,13 @@ class PhpPassword implements PasswordInterface {
   * @see https://www.php.net/password_hash
   */
  public function __construct(
    protected string $algorithm = PASSWORD_DEFAULT,
    protected ?string $algorithm = NULL,
    protected array $options = [],
  ) {
    if (!in_array($algorithm, password_algos(), TRUE)) {
      $this->algorithm = NULL;
      $this->options = [];
    }
  }

  /**
+52 −0
Original line number Diff line number Diff line
@@ -1364,6 +1364,10 @@ public function checkRequirements(string $phase): array {
      }
    }

    if ($phase === 'runtime') {
      $requirements['password_hashing'] = $this->checkPasswordHashing();
    }

    // Ensure that no module has a current schema version that is lower than the
    // one that was last removed.
    if ($phase == 'update') {
@@ -1522,4 +1526,52 @@ public function checkRequirements(string $phase): array {
    return $requirements;
  }

  /**
   * Builds password hashing requirements check result.
   *
   * @return array
   *   Hashing requirements result.
   */
  protected function checkPasswordHashing(): array {
    $availableAlgorithms = password_algos();
    $hashingAlgorithm = \Drupal::getContainer()->getParameter('password.algorithm') ?? PASSWORD_DEFAULT;

    if (!in_array($hashingAlgorithm, $availableAlgorithms, TRUE)) {
      return [
        'title' => $this->t('Password hashing'),
        'value' => $this->t('The configured password hashing algorithm %algorithm is not available in your PHP installation. Ensure that the <a href=":url">necessary PHP extensions</a> are installed and that the Drupal password hashing configuration is correct.', [
          '%algorithm' => $hashingAlgorithm,
          ':url' => 'https://www.php.net/manual/password.requirements.php',
        ]),
        'severity' => RequirementSeverity::Error,
      ];
    }
    if ($hashingAlgorithm !== PASSWORD_BCRYPT) {
      return [
        'title' => $this->t('Password hashing'),
        'value' => $this->t('Passwords are hashed with the %algorithm algorithm.', [
          '%algorithm' => $hashingAlgorithm,
        ]),
        'severity' => RequirementSeverity::Info,
      ];
    }

    if (count(array_intersect(['argon2id', 'argon2i'], $availableAlgorithms)) > 0) {
      return [
        'title' => $this->t('Password hashing'),
        'value' => $this->t('Passwords are hashed with the bcrypt algorithm. Drupal 12 will use argon2id by default. It is recommended to <a href=":url">switch</a> to argon2id.', [
          ':url' => 'https://www.drupal.org/node/3581980',
        ]),
        'severity' => RequirementSeverity::Info,
      ];
    }
    return [
      'title' => $this->t('Password hashing'),
      'value' => $this->t('Passwords are hashed with the bcrypt algorithm. Drupal 12 will use argon2id by default. It is recommended to enable <a href=":url">argon2 password hashing</a> in your PHP installation and to switch to argon2id.', [
        ':url' => 'https://www.php.net/manual/password.requirements.php',
      ]),
      'severity' => RequirementSeverity::Warning,
    ];
  }

}
+44 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\KernelTests\Core\Password;

use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\system\Hook\SystemRequirementsHooks;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests Password System Requirements.
 */
#[Group('Password')]
#[RunTestsInSeparateProcesses]
#[CoversMethod(SystemRequirementsHooks::class, 'checkPasswordHashing')]
class PasswordRequirementsArgon2idTest extends PasswordTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['system'];

  /**
   * {@inheritdoc}
   */
  protected ?string $passwordAlgorithm = PASSWORD_ARGON2ID;

  /**
   * Tests that password hashing generates an info requirement by default.
   */
  public function testRequirementsWithBcrypt(): void {
    $requirements = $this->checkSystemRequirements();
    $this->assertArrayHasKey('password_hashing', $requirements);
    $this->assertSame(RequirementSeverity::Info, $requirements['password_hashing']['severity']);
    $this->assertEquals(
      'Passwords are hashed with the <em class="placeholder">argon2id</em> algorithm.',
      (string) $requirements['password_hashing']['value']
    );
  }

}
Loading