Security & hashing
Why SHA-256, not bcrypt or argon
A refresh token is a 64-char CSPRNG secret drawn from the base64url alphabet — about 384 bits of entropy. A slow password hash adds nothing against a secret that can’t be brute-forced, and would break the indexed unique-equality lookup rotation relies on.
Pepper (defence in depth)
Set hash.key to a high-entropy secret and the at-rest digest becomes an HMAC instead of a plain hash — still one deterministic value under the same unique index, so the lookup and the atomic claim are unchanged. An attacker who exfiltrates the table but not the application secret cannot verify or brute-force any token:
REFRESH_TOKENS_HASH_KEY=base64:your-high-entropy-secretKeep the pepper outside the database (env or a secrets manager) and treat it like APP_KEY. A missing or whitespace-only value means no pepper; a value that is not a string at all throws InvalidTokenConfigurationException rather than silently dropping the pepper. Changing hash.key — setting, rotating or clearing it — invalidates every existing token, and holders must sign in again. There is no online pepper rotation, so pick one up front or plan a re-login window.
Validated hashing config
REFRESH_TOKENS_HASH_ALGO=sha512 # sha256 (default) | sha384 | sha512 — anything else throws
REFRESH_TOKENS_LENGTH=64 # 32–4096 base64url chars — outside the range throwsAll three allowed digests fit the 128-char token_hash column.
Serialization safety
The RefreshToken model hides token_hash and access_reference, so a “your devices” endpoint that serializes session rows never leaks the digest or the access-token reference:
// token_hash and access_reference are in $hidden — safe to return as JSON:
return $user->sessions()->get();Guarantees at a glance
- SHA-2 digest at rest under a unique index; the plaintext is returned once and never persisted.
- Single-query anti-double-spend rotation — works identically on PostgreSQL and SQLite.
- Always-on, race-hardened family revocation on reuse detection — a spent token presented to redeem or to log out.
- Absolute session lifetime, stored per family, so a stolen-but-active session can’t be refreshed forever.
- Guard-scoped redemption — a token presented at another account type’s endpoint is invisible.
- #[SensitiveParameter] on every plaintext parameter; plaintext and hashes are never logged.
- The digest and the CSPRNG plaintext come from the shared crypto-for-laravel package.
What stays host-owned
JWT minting and verification, jti denylisting, user-agent parsing, IP geolocation, HTTP routes and controllers, throttling and cookie transport. They plug in through the AccessTokenRevoker contract, enrich() and the events.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.