Skip to content

Password Hasher

The Password Hasher plugin provides secure password hashing and verification using industry-standard algorithms.

Four adapters are available:

  • Argon2idPasswordHasher uses Argon2id, recommended by OWASP 2024 for new applications.
  • Argon2iPasswordHasher uses Argon2i, optimized for side-channel attack resistance.
  • BcryptPasswordHasher uses bcrypt, a proven algorithm still widely supported.
  • PlaintextPasswordHasher produces predictable hashes for testing (never use in production).

Why Use It

Passwords must never be stored in plain text. The Password Hasher ensures secure hashing with automatic salt generation and timing-safe verification. The test adapter enables predictable fixtures and assertions.

Usage

Hash passwords before storage:

$hash = $this->passwordHasher->hash('secret123');

Verify passwords during authentication:

$isValid = $this->passwordHasher->verify('secret123', $storedHash);

Argon2id is memory-hard, protecting against GPU and ASIC attacks:

$hasher = new Argon2idPasswordHasher(
    memoryCost: 65536,  // 64 MB (default)
    timeCost: 4,        // iterations (default)
    threads: 1,         // parallelism (default)
);
// Hash format: $argon2id$v=19$m=65536,t=4,p=1$...

Argon2i

Argon2i is optimized for resistance to side-channel attacks:

$hasher = new Argon2iPasswordHasher(
    memoryCost: 65536,  // 64 MB (default)
    timeCost: 4,        // iterations (default)
    threads: 1,         // parallelism (default)
);
// Hash format: $argon2i$v=19$m=65536,t=4,p=1$...

Bcrypt

Bcrypt remains a solid choice for compatibility with existing systems:

$hasher = new BcryptPasswordHasher(cost: 12);  // Default is 10
// Hash format: $2y$12$...

Algorithm Comparison

Aspect Argon2id Argon2i Bcrypt
OWASP 2024 Recommended Acceptable Acceptable
Memory-hard Yes Yes No
GPU resistance High High Moderate
Side-channel resistance Moderate High Low
PHP support 7.3+ 7.2+ 5.5+

Testing

The PlaintextPasswordHasher produces predictable hashes:

$hasher = new PlaintextPasswordHasher();
$hash = $hasher->hash('secret123');  // Returns: $plain$secret123
$hasher->verify('secret123', '$plain$secret123');  // true

This enables fixtures with known password hashes and predictable test assertions.

Architecture Pattern

The demo application separates password concerns into two Value Objects:

  • Password: Validates plaintext input (length, complexity rules)
  • HashedPassword: Stores the hashed value from the database

Password hashing and verification happen in the Application layer with explicit dependency injection:

final class AuthenticateUserHandler extends AbstractCommandHandler
{
    public function __construct(
        private readonly UserRepository $userRepository,
        private readonly PasswordHasherInterface $passwordHasher,
        // ...
    ) {}

    public function handle(CommandInterface $command): void
    {
        $user = $this->userRepository->findByEmail($email);

        if (!$this->passwordHasher->verify($command->password, $user->getHashedPassword()->getValue())) {
            throw new InvalidArgumentException('Invalid credentials');
        }
        // ...
    }
}

This approach follows Clean Architecture principles:

Aspect Benefit
Explicit dependencies Visible in constructor, easy to test
No global state No static registry or service locator
Domain purity Value Objects have no service dependencies
Testability Inject PlaintextPasswordHasher for predictable test behavior

Source Files

  • app/demo/User/Domain/Password.php - Plaintext validation
  • app/demo/User/Domain/HashedPassword.php - Stored hash wrapper
  • app/demo/User/Application/Command/AuthenticateUserHandler.php - Verification logic

See Also