Password hashing integration — Argon2 via sodium
Concept
Password hashing is the secure storage of passwords. Never store plaintext passwords. A hash is a one-way function — you can verify a password against a hash, but you can't reverse the hash to get the password.
Modern password hashing algorithms:
- bcrypt: The standard for years. Work factor (cost) parameter — higher = slower = more secure. PHP's
password_hash($pass, PASSWORD_BCRYPT, ['cost' => 12]). - Argon2id: Recommended by OWASP. Memory-hard (resistant to GPU attacks).
password_hash($pass, PASSWORD_ARGON2ID). PHP 7.4+, requires the sodium extension. - SHA-1, MD5: NEVER use for passwords — too fast, easily brute-forced. OK for checksums, not passwords.
PHP password API:
password_hash(string $password, int $algo, array $options = []): Hash a password. Returns the hash (includes the algorithm and salt embedded in the string).password_verify(string $password, string $hash): Verify a password against a hash. Returns bool.password_needs_rehash(string $hash, int $algo, array $options = []): Check if a hash was created with different settings (e.g., after upgrading cost factor).
Salting: PHP's password_hash() generates a random salt automatically and embeds it in the hash string. Never pass your own salt — the built-in random salt is secure.
Sodium extension (ext-sodium): Provides sodium_crypto_pwhash_str() and sodium_crypto_pwhash_str_verify() for Argon2id. Also provides authenticated encryption, secret-key signing, and more.
Timing attacks: password_verify() uses constant-time comparison — it always takes the same time regardless of where the comparison fails. Never use === for hash comparison.
Code Example
<?php
namespace Framework\Auth;
interface HasherInterface
{
public function make(string $value, array $options = []): string;
public function check(string $value, string $hash): bool;
public function needsRehash(string $hash, array $options = []): bool;
}
class BcryptHasher implements HasherInterface
{
public function __construct(private readonly int $cost = 12) {}
public function make(string $value, array $options = []): string
{
$cost = $options['cost'] ?? $this->cost;
$hash = password_hash($value, PASSWORD_BCRYPT, ['cost' => $cost]);
if ($hash === false) {
throw new \RuntimeException('Bcrypt hashing not supported.');
}
return $hash;
}
public function check(string $value, string $hash): bool
{
return password_verify($value, $hash); // constant-time comparison
}
public function needsRehash(string $hash, array $options = []): bool
{
$cost = $options['cost'] ?? $this->cost;
return password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => $cost]);
}
}
class Argon2IdHasher implements HasherInterface
{
public function make(string $value, array $options = []): string
{
$hash = password_hash($value, PASSWORD_ARGON2ID, $options);
if ($hash === false) {
throw new \RuntimeException('Argon2id hashing not supported. Install ext-sodium.');
}
return $hash;
}
public function check(string $value, string $hash): bool
{
return password_verify($value, $hash);
}
public function needsRehash(string $hash, array $options = []): bool
{
return password_needs_rehash($hash, PASSWORD_ARGON2ID, $options);
}
}
// Usage in UserProvider
class EloquentUserProvider
{
public function __construct(
private readonly string $model,
private readonly HasherInterface $hasher,
) {}
public function validateCredentials(\Framework\Auth\Authenticatable $user, array $credentials): bool
{
$plain = $credentials['password'];
if (!$this->hasher->check($plain, $user->getAuthPassword())) {
return false;
}
// Upgrade hash if needed (e.g., after cost factor change)
if ($this->hasher->needsRehash($user->getAuthPassword())) {
$user->setAttribute('password', $this->hasher->make($plain));
$user->save();
}
return true;
}
}
// In a controller
$hash = $hasher->make('user_password123'); // store this
$hasher->check('user_password123', $hash); // true
$hasher->check('wrong_password', $hash); // false