marko/hashing
A secure hashing package for Marko/Laravel PHP apps, offering a driver-based manager for bcrypt, argon2, and other algorithms. Easily hash and verify passwords, generate secure hashes, and swap drivers via simple configuration.
This package is a read-only split of marko-php/marko; all issues, PRs, and docs live at https://github.com/marko-php/marko. Treat this repository as the published artifact only.
Minimal setup:
composer require marko/hashingconfig/app.php equivalent or your bootstrap file). If there's a publishable config, run the standard vendor:publish equivalent so you get a config/hashing.php.bcrypt or argon2id) via config or env.// Registration
$user->password = app(Hasher::class)->hash($request->password);
// Login
if (app(Hasher::class)->verify($request->password, $user->password)) {
// authenticated
}
Where to look first, in order:
Hasher contract (usually Marko\Hashing\Contracts\Hasher) — this is the stable surface you should code against.rounds, memory, time.Hash manager/facade or helper if the framework exposes one.If the repo looks empty or has only a src/ and composer.json, that's expected for a read-only split — go upstream for usage examples and changelogs.
Inject the contract, not a concrete driver. Type-hint Hasher in constructors and resolve it from the container. Hardcoding new BcryptHasher(...) ties you to one algorithm and bypasses config.
final class RegisterUser
{
public function __construct(private Hasher $hasher) {}
public function handle(array $data): User
{
$data['password'] = $this->hasher->hash($data['password']);
return User::create($data);
}
}
Centralize hashing in one place. Hashing inside a controller and a model mutator/observer is the most common double-hash bug. Pick one layer — usually an action/service or an explicit setPasswordAttribute mutator — and hash only there.
Rehash on successful login. Algorithms and cost factors change over time; the package should expose needsRehash($hash). On a valid verify, transparently upgrade:
if ($this->hasher->verify($plain, $user->password)) {
if ($this->hasher->needsRehash($user->password)) {
$user->password = $this->hasher->hash($plain);
$user->save();
}
}
Testing. Hashing is intentionally slow; tests that create hundreds of users will crawl. Two options:
4) — never do this in production.Hasher implementation in the container for tests that don't assert real hashing behavior.For tests that do assert round-tripping, use the real hasher and just a handful of fixtures.
Seeding and ops tooling. For admin users, a CLI command that prompts for a password and prints/stores the hash is safer than hardcoding hashes in seeders. Use the same Hasher so cost settings stay consistent.
Integration with auth packages. If marko/auth (or similar) exists, it may expect a Hasher binding — check it resolves your configured driver rather than a hardcoded one. Wire login flows to verify() only, never to == on hashes.
Tokens are not passwords. Password reset tokens and API tokens should be compared with hash_equals() on a random stored value, or stored hashed with a fast hash (SHA-256) — not with password hashing. Don't route them through this package.
Password column length. Bcrypt output is 60 chars, but Argon2/Argon2id output is longer (~95–100+ chars) and PHP's default can change. Use VARCHAR(255) (or TEXT) so switching drivers doesn't truncate hashes and silently break logins.
Never hash twice. The second hash produces a hash of a hash; verify() will then always fail against the original plaintext. Symptom: registration "works" but every login fails. Audit mutators, observers, and form requests.
verify() returns bool, don't throw on mismatch. Design login flows around a boolean. Also handle users with no password (SSO-only, invited-but-not-set) — a null hash should short-circuit, not be passed to verify().
Cost/rounds tuning is a deployment decision. Raising bcrypt rounds or Argon2 memory/time makes login slower for everyone; measure. Keep dev/test values low for speed, but read production values from env so you can bump them without a code deploy.
Timing safety. Rely on the package's verify(); don't reimplement comparison. It should use PHP's password_verify under the hood, which is constant-time. Hand-rolled hash_equals on stored hashes is a red flag.
Optional pepper/secret. If the driver supports a global secret (pepper), document it and store it outside the DB. Rotating it invalidates every existing hash, so treat rotation as a migration, not a config tweak.
Logging. Never log plaintext passwords, request payloads containing them, or full hashes at info level. Exception handlers that dump request data are a common leak.
Testing speed trap. Lowering rounds in a .env.testing is fine — accidentally shipping that config is not. Add a guard/assertion that production never runs at test cost.
Extension points. To add a custom algorithm (e.g. scrypt via a library), implement the Hasher contract and register it as a driver with the manager, then select it by name in config. This keeps call sites unchanged.
Read-only repo reality. You cannot open a PR here. File upstream at marko-php/marko; also pin a version in composer.json since this split's release cadence follows upstream and a dev-main constraint can drift.
Debugging checklist when verification fails: confirm the stored value starts with the expected prefix ($2y$ for bcrypt, $argon2id$ for Argon2id), confirm the column is long enough, confirm you aren't trimming/normalizing the password differently at registration vs login, and confirm the driver in config is the same one used to create the hash.
How can I help you explore Laravel packages today?