graham-campbell/passwd
A Laravel password validator and generator that checks strength, requires letters, numbers, and symbols, rejects common or weak passwords, and generates secure random passwords via a simple, configurable service provider integration.
Before anything else: this package is archived under the GrahamDeprecated namespace, its last release is 0.4.0-alpha (Laravel 4.1 era), and it has zero dependents. Your first step is not composer require — it is verifying it can coexist with your lockfile at all. Run it in a throwaway branch, pinned to the exact alpha tag:
composer require "graham-campbell/passwd:0.4.0-alpha" --dry-run
Never use ^0.4 on a pre-1.0 hashing library. If Composer conflicts on illuminate/*, stop immediately — Laravel's own Hash facade already wraps password_hash/password_verify, and you have lost nothing.
What you're actually installing. A thin manager over PHP's native password functions, deliberately split into two roles:
That split is the value proposition. It lets you produce new hashes with a modern algorithm while still verifying old ones written by something else (WordPress phpass, forum hashes, etc.). If you don't need multiple checkers, you don't need this package.
Read in this order:
src/ — the manager class exposing hash() and check(), then the Hashers/ and Checkers/ directories in full. It's a small surface; know exactly what your credentials flow through.Wire it up the 4.1 way in app/config/app.php and publish the config:
'providers' => [
// ...
'GrahamCampbell\Passwd\PasswdServiceProvider',
],
'aliases' => [
// ...
'Passwd' => 'GrahamCampbell\Passwd\Facades\Passwd',
],
First smoke test in artisan tinker:
$hash = Passwd::hash('secret');
Passwd::check('secret', $hash); // true
Passwd::check('wrong', $hash); // false
If that round-trips, the plumbing works and everything after this is about where you call it.
The only first use case worth the install: a bounded legacy credential migration — an import job or internal tool that needs to verify imported hashes once, at account-claim time. That is where the hasher/checker split earns its keep. Using it as the primary hasher for new signups on a revenue path is a different decision, and a worse one.
Pattern 1 — direct swap in auth. Replace Hash::make/Hash::check with Passwd::hash/Passwd::check in registration, login, and password-reset. Do it in one commit and have a test asserting an existing stored hash still verifies. The manager is a pass-through, so the risk is not behavioural — it is that you now own an unmaintained library for every credential in the system.
Pattern 2 — verify-any, rehash-on-success (the one that justifies the package). For a legacy import, register a checker per historical format and let the manager try them in order. Lifecycle: verify with the old checker → if the hash is stale, rehash with the current hasher → persist → continue login. The discipline is verify first, write second:
$user = User::where('email', $email)->first();
if ($user && Passwd::check($plain, $user->password)) {
if (/* stale hash: wrong algorithm, low cost, legacy format */) {
$user->password = Passwd::hash($plain);
$user->save();
}
Auth::login($user);
}
The plaintext is in memory only at this instant. Do not stash it, do not hand it to a queued job for the rehash, do not log the old hash next to it.
Pattern 3 — adapter boundary. Given the package's status, wrap it. One interface, one implementation, bound in the container:
interface PasswordHasher
{
public function hash($plain);
public function check($plain, $hash);
}
Your app calls $this->hasher->check(...) and never sees a Passwd facade reference. When the package rots past the point of patching — or the day you migrate off 4.1 — the blast radius is one class and one binding. Twenty minutes now, a sprint later.
Pattern 4 — model-level encapsulation. Rather than sprinkling Passwd:: across controllers, put both operations on the user model (setPasswordAttribute, checkPassword) or in a CredentialService. One place to enforce the rehash policy, one place to change cost factors, one place to fall back to the Hash facade if you decide to hedge.
Pattern 5 — testing. Because the manager is container-bound, swap the binding in controller tests for a deterministic stub — no bcrypt CPU cost per request:
$this->app->bind('Passwd', function () {
return new FakePasswd(); // hash() => 'x', check() => true
});
Keep at least one real integration test that round-trips an actual hash through the actual package. The stub proves controller logic; only the integration test proves credentials work.
Pattern 6 — custom hasher/checker registration. The extension points are Hashers/ and Checkers/ plus config. If you must support a bespoke legacy format, implement against the existing checker contract instead of branching inside your controller. That keeps the "try each format, in order" logic in the manager and your legacy code in one class you can delete when the migration window closes.
Migration tip. Run the legacy checker only for flagged users (legacy_hash = 1, set at import time). Trying three checkers on every login for the full user base is wasted CPU and an unbounded surface for format-detection bugs. When the flag clears, delete the legacy path.
The upstream is retired. GrahamDeprecated/..., zero dependents, v0.4.0-alpha. No CVE patches, no PHP compatibility fixes, no issue-tracker answers. Practically: you own the fork whether or not you admit it. Budget for it or avoid it.
Alpha means the API is not a contract. hash()/check() are safe bets because they mirror the native functions, but the manager's construction, config keys, and checker-registration surface can all move between tags. Pin the version and commit composer.lock.
Laravel 4.1 snippets will bite if you're porting from modern blog posts. No $this->app->singleton sugar in the shape you expect, no ::class in config arrays on older PHP, providers registered by string in app.php. Copy patterns from the package's own README, not from a 2023 tutorial.
Argon2 is a build-time dependency. PASSWORD_ARGON2I/PASSWORD_ARGON2ID are only defined if PHP was compiled with Argon2 support. On stock distro packages circa the 4.1 era, they usually were not. Guard with defined('PASSWORD_ARGON2I') before setting it as default — otherwise you get a runtime failure on the signup path, which is the worst possible place to discover it.
Bcrypt silently truncates at 72 bytes. Not the package's fault, but it won't stop you. If you accept long passphrases, either pre-hash (SHA-256 then bcrypt) or move to Argon2 — and write it down, because it is unrecoverable information if you switch later.
Cost tuning is a benchmark, not a guess. Time Passwd::hash() in a loop on production-class hardware; target 100–250ms. On an old shared VM the default may already be too slow and you'll be tempted to lower it — resist; lowering bcrypt cost is a straight security downgrade. Also note hashing is CPU-bound: under login bursts your failure mode is a slow worker queue, not a crash.
Rehash timing is the classic bug. Verify the plaintext before you hash and save. And when you do save, make sure the new hash does not re-trigger your setPasswordAttribute mutator into hashing the already-hashed value — that produces a hash-of-a-hash and locks the user out next login. Symptom: the user who just logged in successfully cannot log in tomorrow.
Never compare hashes with == and never log them. Use check() — it wraps password_verify, which is timing-safe. Hashes leak through logs, exception traces, and ->toArray() dumps into API responses. Add password to $hidden on every model that carries one.
Config quirks. The published config is the source of truth for the default hasher, and the facade does not let you override per call cleanly. If you need two hashers at once (new writes vs. legacy verifies), do not fight the config — bind your own instance of the concrete hasher in a small service and call the checker chain explicitly. You are now writing the ~50 lines the package was supposed to save you, which is a data point on whether the dependency is pulling its weight.
Write the exit plan down now. password_hash/password_verify are in the platform. With the adapter from Pattern 3, leaving this package is a one-class change: swap the body to password_hash($plain, PASSWORD_BCRYPT, ['cost' => N]) and password_verify($plain, $hash). If you cannot articulate that swap in a paragraph, you have coupled to a retired alpha library harder than it deserves.
How can I help you explore Laravel packages today?