Custom hash verifiers
Custom hash verifiers
Password verification for bulk import and lazy migration
goes through a deny-by-default registry of HashVerifiers. The contract is
tiny:
namespace Cbox\Id\Identity\Contracts;
interface HashVerifier
{
public function supports(string $hash): bool; // recognizes this format?
public function verify(string $password, string $hash): bool;
public function needsRehash(string $hash): bool; // upgrade to the platform hasher?
}
The package ships exactly one implementation, NativePasswordVerifier, covering
the families PHP's password_hash() produces — bcrypt and argon2 — verified
through the vetted password_verify / password_needs_rehash. Nothing is
hand-rolled.
The deny-by-default argument
HashVerifierRegistry (bound to the HashVerifier contract) tries each registered
verifier in order and asks the first that supports() the format to decide. If
no verifier supports a hash, verify() returns false. An unknown format is a
rejection, never a silent pass.
This matters because the alternative — "we don't recognize it, so let them in" —
turns a migration into a backdoor. It also means importing a user whose hash
nothing can verify is caught up front (ImportOptions::$rejectUnverifiableHashes,
on by default, makes it a per-row error) instead of creating an account that can
never authenticate.
Add a format
To accept, say, Firebase's scrypt during a migration, register a verifier that wraps a vetted implementation of that KDF — never a hand-written one:
namespace App\Auth;
use Cbox\Id\Identity\Contracts\HashVerifier;
class FirebaseScryptVerifier implements HashVerifier
{
public function supports(string $hash): bool
{
return str_starts_with($hash, 'firebase-scrypt$');
}
public function verify(string $password, string $hash): bool
{
if (! $this->supports($hash)) {
return false; // stay deny-by-default for anything else
}
// Delegate to a vetted library configured with your project's
// signer key / salt separator, and compare in constant time.
return /* $vettedLibrary->verify($password, $hash) */;
}
public function needsRehash(string $hash): bool
{
return true; // a foreign format should always upgrade to the platform hasher
}
}
Register it in config/cbox-id.php — it is appended after the native verifier:
'hashing' => [
'verifiers' => [
App\Auth\FirebaseScryptVerifier::class,
],
],
That is the entire seam. Imported users with that format now sign in on day one,
and each hash is transparently re-hashed to argon2id on first successful login
(needsRehash() returning true drives the upgrade). Once your users have logged
in, the verifier has nothing left to do and can be removed.
Keep a custom verifier as a short migration bridge, not a permanent credential store — and only ever back it with a maintained, vetted library.