Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Password Rehashing and Argon2id Setup

By Kokil Thapa | Last reviewed: September 2026

Stored passwords age badly. A Laravel authentication stack that still writes bcrypt hashes with low cost factors—or that never upgrades hashes after you change config/hashing.php—leaves user credentials weaker than modern guidance recommends. Laravel Password Rehashing and Argon2id Setup solves both problems: Argon2id becomes your default algorithm, and Laravel rehashes each password transparently the next time the user logs in with the correct plaintext. You do not need a disruptive reset campaign for most applications. This guide covers configuration on Laravel 12 and 13 with PHP 8.3 or higher, the exact rehash trigger points, production tuning, and the mistakes I have seen on client portals and eCommerce apps.

What is Laravel password rehashing and why should you use Argon2id?

Password hashing is one-way: you never store plaintext, and you never decrypt. When a user registers or changes a password, Laravel calls Hash::make(), which delegates to PHP's password_hash() using the driver defined in config/hashing.php. Verification uses Hash::check($plain, $hash).

Rehashing means: the stored hash is still valid, but its parameters—or the algorithm itself—are outdated relative to your current config. Laravel detects that with Hash::needsRehash($storedHash) and, on a successful login, writes a fresh hash without asking the user to pick a new password. That is the core of a zero-downtime migration from bcrypt to Argon2id.

Argon2id won the Password Hashing Competition and is the recommended choice in the OWASP Password Storage Cheat Sheet. It combines side-channel resistance from Argon2i with GPU-hardness from Argon2d. Bcrypt remains acceptable for legacy systems, but for greenfield Laravel 13 apps on PHP 8.5—or existing Laravel 12 deployments on PHP 8.3—Argon2id is the sensible default in 2026.

Password Rehashing on LoginUser Loginemail + passwordHash::checkverify bcryptneedsRehashdriver changedHash::makeargon2idSessionGuard saves new hash to users.passwordUser sees no difference — same session, stronger storageFailed check = no rehash (prevents oracle leaks)Only successful auth triggers upgrade
Laravel password rehashing and Argon2id setup: automatic upgrade path after successful credential verification

On legal-tech portals and client dashboards—systems like those described in our Mijar Law Associates portfolio case—users log in infrequently but expect document access to remain secure for years. Rehashing at login closes the gap between "we changed config last month" and "every stored hash still reflects 2019 bcrypt rounds."

Bcrypt vs Argon2id in Laravel

Criterionbcryptargon2id
PHP constantPASSWORD_BCRYPTPASSWORD_ARGON2ID
GPU resistanceModerateStrong (memory-hard)
Tunable axesCost factor (rounds)Memory, time, threads
Laravel driver keybcryptargon2id
Typical migrationLegacy defaultTarget for 2026 apps
Rehash triggerSame needsRehash() APISame needsRehash() API

Both drivers produce strings that Hash::check() verifies correctly regardless of which driver is currently configured. That backward compatibility is what makes lazy migration practical.

How do you configure Argon2id as the default Laravel hash driver?

Start with server prerequisites. Argon2id requires PHP compiled with Argon2 support—verify before changing production config:

php -r "var_dump defined('PASSWORD_ARGON2ID');"
php -m | grep -i sodium

You should see bool(true). On Ubuntu 24.04 with PHP 8.5, the standard php8.5-fpm package includes this. If the constant is missing, install the appropriate PHP build or enable sodium before proceeding. Our Ubuntu server setup guide covers multi-version PHP installs on production boxes.

Step 1: Environment and config

In Laravel 12 and 13, edit .env:

HASH_DRIVER=argon2id

Then confirm config/hashing.php (structure is consistent across recent releases—consult the Laravel hashing documentation for your exact version):

return [
    'driver' => env('HASH_DRIVER', 'bcrypt'),

    'bcrypt' => [
        'rounds' => env('BCRYPT_ROUNDS', 12),
        'verify' => true,
    ],

    'argon' => [
        'memory' => 65536,
        'threads' => 1,
        'time' => 4,
        'verify' => true,
    ],

    'argon2id' => [
        'memory' => env('ARGON_MEMORY', 65536),
        'threads' => env('ARGON_THREADS', 1),
        'time' => env('ARGON_TIME', 4),
        'verify' => true,
    ],

    'rehash_on_login' => true,
];

Clear config cache after deploy:

php artisan config:clear
php artisan config:cache

New registrations and password resets immediately receive Argon2id hashes. Existing bcrypt users stay on bcrypt until they authenticate—exactly the behaviour you want.

Step 2: Verify in Tinker

php artisan tinker

>>> $hash = Hash::make('test-password-123');
>>> str_starts_with($hash, '$argon2id$');
=> true

>>> Hash::check('test-password-123', $hash);
=> true

If the prefix shows $2y$, your config cache is stale or HASH_DRIVER is not loaded in that environment.

Argon2id Stack in LaravelPHP 8.3+PASSWORD_ARGON2ID.envHASH_DRIVERconfig/hashing.phpmemory, timeHash facademake() / check()Application touchpointsRegistrationPassword resetLogin rehashManual checkusers.password column (varchar 255)
Layers involved in Laravel Password Rehashing and Argon2id Setup—from PHP extension through config to runtime calls

When does Laravel automatically rehash passwords during authentication?

Automatic rehashing is not magic middleware you must install—it lives in the authentication guard. When a user logs in through the default session guard, Laravel validates credentials, then checks whether the stored hash matches current policy.

The flow inside Illuminate\Auth\SessionGuard (paraphrased for clarity):

  1. Retrieve user by credentials.
  2. Verify plaintext with Hash::check() against $user->password.
  3. If valid and Hash::needsRehash($user->password) returns true, hash the plaintext again with the current driver and persist it.
  4. Establish the authenticated session.

This runs for standard Auth::attempt() calls and Fortify/Breeze login routes that use the session guard. It does not run for API token authentication via Sanctum—token auth never sees the password after initial issuance. If your REST API shares the same user table, mobile clients keep working; only password-based flows trigger rehashing.

Custom login controllers

If you bypass Auth::attempt() and manually validate—a pattern I still see in older codebases—you must rehash yourself:

use Illuminate\Support\Facades\Hash;

if (! Hash::check($request->password, $user->password)) {
    throw ValidationException::withMessages([
        'email' => __('auth.failed'),
    ]);
}

if (Hash::needsRehash($user->password)) {
    $user->forceFill([
        'password' => Hash::make($request->password),
    ])->save();
}

Auth::login($user, $request->boolean('remember'));

Never rehash when Hash::check() fails. Doing so would leak whether a user exists and create corrupt hashes.

Password reset and change flows

Password reset always writes a fresh hash via Hash::make() with the current driver—no needsRehash() call required. The same applies to profile "change password" forms. In my experience working on production Laravel applications, reset-heavy months (after a security announcement) accelerate Argon2id adoption because those users never need a login-time upgrade.

Before vs After MigrationBefore (mixed)User A: $2y$12$...bcrypt, 2022 signupUser B: $2y$10$...bcrypt, low roundsUser C: never logged instill bcrypt until loginConfig says argon2idstorage not aligned yetAfter (lazy upgrade)User A: $argon2id$...rehashed on loginUser B: $argon2id$...rehashed on loginUser C: $2y$10$...waits for next loginNo mass reset requiredactive users upgraded firstdeploy
Lazy bcrypt-to-Argon2id migration: Laravel password rehashing upgrades active accounts first without forcing inactive users to reset

How do you migrate existing bcrypt users to Argon2id without forcing a reset?

The lazy migration strategy is the production default for good reason. You change config, deploy, and let logins do the work. For a Laravel eCommerce platform with tens of thousands of customer accounts, a forced reset would spike support tickets and cart abandonment. Rehash-on-login avoids that.

Monitoring migration progress

Run a read-only audit query against MySQL 9.7 or PostgreSQL 18—prefix patterns tell you algorithm distribution:

SELECT
  CASE
    WHEN password LIKE '$argon2id$%' THEN 'argon2id'
    WHEN password LIKE '$2y$%' THEN 'bcrypt'
    WHEN password LIKE '$2a$%' THEN 'bcrypt'
    ELSE 'other'
  END AS algo,
  COUNT(*) AS total
FROM users
GROUP BY algo;

Schedule this weekly via a read-only scheduled Artisan command until bcrypt counts plateau near zero. Log results; do not expose them publicly.

Dormant accounts

Users who have not logged in since your deploy keep bcrypt hashes indefinitely. That is acceptable for most businesses—the hash is still one-way. Options for stricter policies:

  • Force password reset on next login via a password_changed_at flag (custom middleware).
  • Expire dormant accounts per your retention policy.
  • After 12–18 months, email inactive users a voluntary reset link—not a blind invalidation.

On a legal-tech portal I built, infrequent client logins made lazy migration the only realistic path; partners would not tolerate a blanket reset before a court deadline.

Bulk rehash is impossible (by design)

You cannot convert bcrypt to Argon2id offline without the plaintext password. Any tool claiming otherwise is misunderstanding password hashing. The PHP password_hash() documentation is explicit: verification needs the original input. Plan for login-time upgrades, not batch SQL updates.

How do you tune Argon2id memory and time cost for production Laravel apps?

Argon2id parameters trade security against login latency and RAM. OWASP suggests targeting roughly 600ms hash time on your production hardware. Measure—not guess.

Benchmark on the server

php artisan tinker

>>> $start = hrtime(true);
>>> Hash::make('benchmark-password');
>>> (hrtime(true) - $start) / 1e6;
=> 487.3

Run this on the same PHP-FPM workers that serve auth—not your laptop. Shared hosting with tight memory limits may reject high memory values; start at 65536 KiB (64 MB) and increase if latency stays under your budget.

ParameterEnv keyConservative startHigher security
Memory (KiB)ARGON_MEMORY65536131072–262144
Time (iterations)ARGON_TIME45–6
ThreadsARGON_THREADS12 (match CPU cores cautiously)

Raising parameters marks existing Argon2id hashes for rehash on next login—the same needsRehash() mechanism. That is useful when you harden policy annually; plan deploys outside peak login windows.

PHP-FPM and worker memory

Each concurrent login allocates Argon2 memory inside the worker. On a 2 GB VPS running PHP 8.4 with 10 FPM children, aggressive ARGON_MEMORY=262144 can cause OOM under burst login traffic. Monitor with pm.max_children headroom. For high-traffic apps, consult our testing and optimization service or load-test auth endpoints before promoting config changes.

Argon2id Tuning Decision TreeBenchmark on serverlatency?< 300msincrease memory/time> 800msreduce parametersOOM errors?lower memory or FPM300–800ms OKship to productionUsers complainprofile peak trafficRe-test after PHP-FPM or hardware changeneedsRehash upgrades users on next login
Production tuning workflow for Laravel Password Rehashing and Argon2id Setup—balance latency, RAM, and security targets

Security adjacent hardening

Strong hashing is one layer. Pair Argon2id with:

For PostgreSQL-backed Laravel apps, ensure the password column remains at least varchar(255)—Argon2id strings can exceed bcrypt length at high memory settings.

What production mistakes break Laravel password rehashing?

These show up repeatedly during audits and post-deploy support calls:

  1. Cached config with old driver. Deploy sets HASH_DRIVER=argon2id but config:cache was built from stale CI artefacts. Symptom: new hashes still show $2y$.
  2. Custom auth without rehash logic. Replacing Fortify/Breeze login but forgetting the needsRehash block leaves bcrypt forever.
  3. Mass-assignment guards. If password is not fillable and you use update() instead of forceFill(), rehash silently fails. Check logs.
  4. Read replicas and replication lag. Rehash writes to primary; immediate re-auth against a lagging replica sees old hash—rare but confusing. See read replica setup for session stickiness patterns.
  5. Importing users from WordPress 7.1. WordPress may use phpass or bcrypt variants. Laravel verifies many formats, but plan a reset or login-time upgrade—do not assume Argon2id config rewrites imported hashes automatically without login.

After changing hashing policy on sister sites that share Deployer 7 pipelines, I always verify one staging login end-to-end before swapping the production symlink. The extra five minutes beats explaining to a law firm why partners cannot access uploaded affidavits.

If you maintain legacy Laravel 11 apps, note that 11 reached EOL in March 2026—upgrade to Laravel 12 (PHP 8.2+) or 13 (PHP 8.3+) before investing in hashing hardening. Framework security support matters as much as algorithm choice. Read why Laravel fits Nepali business constraints for upgrade prioritisation context.

Key Takeaways

  • Set HASH_DRIVER=argon2id, tune ARGON_MEMORY/ARGON_TIME, and clear config cache so new hashes use Argon2id immediately.
  • Laravel rehashes outdated passwords automatically on successful session login via Hash::needsRehash()—custom auth must replicate that logic.
  • Lazy migration from bcrypt requires no plaintext access; monitor prefix counts in SQL until bcrypt rows approach zero.
  • Benchmark hash duration on production PHP-FPM workers and target roughly 300–800ms—not desktop Tinker results.
  • Combine Argon2id with 2FA, rate limiting, and regular framework upgrades for defence in depth.
  • Never rehash failed login attempts; never batch-convert hashes without user passwords.

People Also Ask

Does Laravel 13 support Argon2id out of the box?

Yes. Laravel 13 on PHP 8.3 or higher includes an argon2id driver in config/hashing.php. Set HASH_DRIVER=argon2id and confirm PASSWORD_ARGON2ID exists in your PHP build. No Composer packages are required beyond the framework.

Will changing to Argon2id log out existing users?

No. Existing sessions remain valid. Only the stored hash changes after the next successful password login. Users keep browsing; the upgrade happens silently in the background during authentication.

Can Sanctum or Passport API tokens trigger password rehashing?

No. Token-based API auth does not present the user password on each request, so needsRehash() never runs. Users who only use mobile tokens stay on bcrypt until they log in through a password form or reset their password.

Is bcrypt still safe if I cannot enable Argon2id on shared hosting?

Bcrypt with cost 12 or higher remains acceptable for many applications. Increase BCRYPT_ROUNDS and enable rehash-on-login so raising rounds upgrades users over time. Move to Argon2id when your host supports it or when you migrate to a VPS—our Linux system administration service handles that transition regularly.

Ship stronger credentials without disrupting users

Laravel Password Rehashing and Argon2id Setup is a low-risk, high-value hardening step: one config change, automatic upgrades at login, and measurable migration via simple SQL. Pair it with 2FA on sensitive portals—like those we document for Notary Nepal and Court Marriage In Nepal—and you close the most common gap between "we take security seriously" and what is actually stored in users.password.

Need Argon2id configured, auth audited, or a Laravel 12/13 upgrade planned before hashing changes go live? Review our Laravel web development services or ongoing support packages, browse the full portfolio, and contact us with your stack details—PHP version, user count, and whether you use Fortify, Breeze, or custom auth.

Frequently Asked Questions

Laravel password rehashing upgrades stored hashes when their algorithm or parameters are outdated. Hash::needsRehash() detects stale bcrypt or Argon2id hashes; on successful login, Laravel writes a fresh hash using current config/hashing.php settings without asking the user for a new password.

Argon2id won the Password Hashing Competition and is recommended in the OWASP Password Storage Cheat Sheet. It combines side-channel resistance from Argon2i with GPU-hardness from Argon2d, giving stronger memory-hard protection than bcrypt's cost-factor rounds alone. Bcrypt remains acceptable for legacy systems, but for Laravel 12 on PHP 8.3 or Laravel 13 on PHP 8.5, Argon2id is the sensible 2026 default. Laravel verifies both formats via Hash::check(), so lazy migration from bcrypt is practical.

First verify PHP support: run php -r "var_dump(defined('PASSWORD_ARGON2ID'));" and confirm bool(true). On Ubuntu 24.04 with PHP 8.5, the standard php8.5-fpm package includes this. Set HASH_DRIVER=argon2id in .env, tune ARGON_MEMORY, ARGON_THREADS, and ARGON_TIME in config/hashing.php, and ensure rehash_on_login is true. After deploy, run php artisan config:clear and php artisan config:cache. New registrations and password resets immediately receive Argon2id hashes; existing bcrypt users upgrade on next login.

Rehashing runs inside Illuminate\Auth\SessionGuard during standard session login—not via separate middleware. After Hash::check() succeeds, Laravel calls Hash::needsRehash() on the stored hash; if true, it re-hashes the plaintext with the current driver and persists it before establishing the session. This applies to Auth::attempt() and Fortify or Breeze login routes using the session guard. Password reset and profile change forms always write fresh hashes via Hash::make(). Sanctum token auth never triggers rehash because the password is not presented on each request.

No. Existing authenticated sessions remain valid. Only the stored database hash changes after the next successful password login. Users keep browsing normally while the upgrade happens silently during authentication.

Yes, using lazy migration—the production default. Change config, deploy, and let logins do the work. Hash::check() verifies bcrypt hashes regardless of current driver, then needsRehash() upgrades them on successful authentication. For eCommerce apps with tens of thousands of accounts, this avoids support spikes and cart abandonment. Dormant accounts keep bcrypt indefinitely, which is acceptable since the hash remains one-way. Stricter options include forcing reset on next login via a password_changed_at flag or emailing voluntary reset links to inactive users after 12–18 months.

OWASP suggests targeting roughly 600ms hash time on production hardware—measure, do not guess. Benchmark on PHP-FPM workers, not your laptop: run Hash::make() in Tinker with hrtime() timing. Start conservatively at ARGON_MEMORY=65536 KiB, ARGON_TIME=4, ARGON_THREADS=1. Increase memory toward 131072–262144 or time toward 5–6 if latency stays under budget. Each concurrent login allocates Argon2 memory inside the worker; on a 2 GB VPS with 10 FPM children, aggressive memory settings can cause OOM under burst login traffic. Raising parameters also marks existing Argon2id hashes for rehash on next login.

Yes. Laravel 13 on PHP 8.3 or higher includes an argon2id driver in config/hashing.php. Set HASH_DRIVER=argon2id and confirm PASSWORD_ARGON2ID exists in your PHP build. No extra Composer packages are required.

PHP must be compiled with Argon2 support. Verify with php -r "var_dump(defined('PASSWORD_ARGON2ID'));" expecting bool(true), and check php -m | grep -i sodium. If the constant is missing, install the appropriate PHP build or enable sodium before changing production config. The article covers Laravel 12 and 13 with PHP 8.3 or higher; on Ubuntu 24.04, the standard php8.5-fpm package includes Argon2 support. Never deploy HASH_DRIVER=argon2id until this check passes on every application server.

No. Token-based API authentication does not present the user password on each request, so Hash::needsRehash() never runs during Sanctum or Passport flows. Mobile clients and REST API consumers keep working against the same user table; only password-based session login triggers rehashing. Users who authenticate exclusively via tokens stay on bcrypt until they log in through a password-based web flow.

No, and that is by design. Password hashing is one-way—you cannot convert bcrypt to Argon2id offline without the plaintext password. PHP's password_hash() documentation is explicit: verification requires the original input. Any tool claiming otherwise misunderstands password hashing. Plan for login-time upgrades via needsRehash(), not batch SQL updates. Password reset flows accelerate adoption because Hash::make() always uses the current driver, upgrading those users without requiring a normal login.

Cached config with the old driver is the most common—deploy sets HASH_DRIVER=argon2id but config:cache was built from stale CI artefacts, so new hashes still show $2y$. Custom auth controllers that bypass Auth::attempt() without a needsRehash block leave bcrypt forever. Mass-assignment guards can silently block rehash if password is not fillable and you use update() instead of forceFill(). Read replica lag can cause rare confusion when rehash writes to primary but immediate re-auth hits a lagging replica. WordPress 7.1 imports may use phpass or bcrypt variants—Laravel verifies many formats, but Argon2id config does not rewrite imported hashes without login.

Use Tinker on the target environment: Hash::make('test-password-123') should return a string starting with $argon2id$, and Hash::check() must return true against it. If the prefix shows $2y$, your config cache is stale or HASH_DRIVER is not loaded in that environment. After changing hashing policy, verify one staging login end-to-end before swapping the production symlink. Always run php artisan config:clear and php artisan config:cache after deploy so production workers read the updated driver.

Run a read-only audit query grouping users by hash prefix: $argon2id$ for Argon2id, $2y$ or $2a$ for bcrypt. Schedule this weekly via a read-only Artisan command until bcrypt counts plateau near zero. Log results internally—do not expose algorithm distribution publicly. On legal-tech portals where clients log in infrequently, expect bcrypt rows to persist longer; that is normal for lazy migration. Reset-heavy months after security announcements accelerate adoption because password reset always writes fresh Argon2id hashes.

If you bypass Auth::attempt() and manually validate credentials, you must replicate SessionGuard rehash logic yourself. After Hash::check() succeeds, call Hash::needsRehash() on the stored hash; if true, use forceFill with Hash::make() and save before Auth::login(). Never rehash when Hash::check() fails—doing so leaks whether a user exists and creates corrupt hashes. This pattern is required in older codebases that replaced Fortify or Breeze login routes. Pair the rehash block with rate limiting on login routes and two-factor authentication for admin and client portals as defence in depth alongside Argon2id.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: