Developer Guide

🔄 Password Rehashing on Login: Migrate bcrypt to Argon2id (2026)

By Ateeq Y Tanoli · 12 July 2026 · 8 min read · 1,553 words

You have inherited a database full of bcrypt hashes at cost 10 — or worse, unsalted SHA-256 — and you want to move everyone to Argon2id. But you cannot decrypt a hash to re-hash it, and forcing 4 million users to reset their passwords is a support nightmare. The answer is rehash-on-login: upgrade each hash silently, one authentication at a time. This guide shows you the exact pattern, with production code in Node.js, Python, and Go.

Definition: Password rehashing is the process of re-computing a stored password hash with a stronger algorithm or a higher work factor. Because the plaintext password is only available for a fraction of a second during login, rehash-on-login upgrades each user's hash transparently the next time they authenticate — with zero downtime and no password reset.

What Is Password Rehashing on Login?

A password hash is a one-way transform: you cannot reverse it to recover the plaintext. That is the whole point. But it also means you cannot bulk-convert bcrypt hashes into Argon2id hashes in a background job — you simply do not have the input the new algorithm needs.

Login is the one moment the plaintext exists in memory. The user types their password, you verify it against the stored hash, and for that instant you hold the raw value. Rehash-on-login exploits that window: immediately after a successful verification, you re-hash the plaintext with your target algorithm and overwrite the stored hash. The user is authenticated, nothing on their end changes, and the row is now upgraded.

Why Rehash-on-Login Matters in 2026

Hashing parameters that were safe five years ago are now weak. GPU cracking speeds compound relentlessly, and a static work factor loses ground every year. Rehash-on-login is how you keep pace without a disruptive migration.

The urgency is not theoretical. According to the Verizon 2026 Data Breach Investigations Report, roughly 86% of web application breaches involve stolen or brute-forced credentials. When a database leaks, the difference between bcrypt cost 8 and Argon2id at 64 MiB is the difference between mass account takeover in hours and cracking that is economically infeasible.

The standards bodies agree on the destination. The OWASP Password Storage Cheat Sheet names Argon2id as its first-choice algorithm, stating that it “should be used where FIPS-140 compliance is not required.” NIST SP 800-63B likewise requires that stored secrets be protected with a “suitable one-way key derivation function” using memory-hard parameters. For the algorithm comparison behind these recommendations, see our Argon2 vs bcrypt developer guide.

The Rehash-on-Login Pattern

Every rehash-on-login flow follows the same five steps, regardless of language:

  1. Look up the stored hash for the submitted username or email.
  2. Identify the algorithm from the hash prefix ($2b$ = bcrypt, $argon2id$ = Argon2id, and so on).
  3. Verify the submitted plaintext against the stored hash using the matching verifier.
  4. Decide whether the hash needs upgrading: wrong algorithm, or correct algorithm but outdated parameters.
  5. Rehash and persist the plaintext with current parameters, but only after verification has already succeeded.

The critical rule: never rehash before you have confirmed the password is correct. Rehashing a wrong password would overwrite a valid hash with garbage and lock the user out permanently.

Implementation: Node.js, Python, Go

Modern password libraries ship a “needs rehash” helper precisely for this pattern. Here is the same logic in three ecosystems.

Node.js with argon2

import * as argon2 from 'argon2';

const OPTS = { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 1 };

async function login(user, password) {
  // Legacy bcrypt hashes start with $2; verify with bcrypt first.
  const isBcrypt = user.hash.startsWith('$2');
  const ok = isBcrypt
    ? await bcrypt.compare(password, user.hash)
    : await argon2.verify(user.hash, password);

  if (!ok) return false;                       // wrong password: stop here

  // Upgrade if it is bcrypt, or Argon2id with stale params.
  if (isBcrypt || argon2.needsRehash(user.hash, OPTS)) {
    const newHash = await argon2.hash(password, OPTS);
    await db.updateHash(user.id, newHash);     // overwrite in place
  }
  return true;
}

Python with argon2-cffi

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
import bcrypt

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1, hash_len=32)

def login(user, password: str) -> bool:
    is_bcrypt = user.hash.startswith("$2")
    if is_bcrypt:
        if not bcrypt.checkpw(password.encode(), user.hash.encode()):
            return False
    else:
        try:
            ph.verify(user.hash, password)
        except VerifyMismatchError:
            return False

    # check_needs_rehash() returns True when parameters are below current config
    if is_bcrypt or ph.check_needs_rehash(user.hash):
        db.update_hash(user.id, ph.hash(password))
    return True

Go with golang.org/x/crypto

func Login(user User, password string) bool {
    isBcrypt := strings.HasPrefix(user.Hash, "$2")
    if isBcrypt {
        if bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(password)) != nil {
            return false
        }
    } else if !argon2Verify(user.Hash, password) { // your Argon2id decoder
        return false
    }

    if isBcrypt || argon2NeedsRehash(user.Hash) {
        newHash := argon2idEncode(password) // m=64MiB, t=3, p=1, 32-byte salt
        db.UpdateHash(user.ID, newHash)
    }
    return true
}

Note that Go's standard bcrypt package exposes bcrypt.Cost(hash) so you can rehash when the stored cost is below your target even without switching algorithms. Whatever you generate for new users should still start from high-entropy input — our CSPRNG-secure password generator and its password API produce exactly that.

Detecting Legacy Hashes

Reliable migration depends on cleanly identifying what algorithm produced each stored hash. Modern password hashes are self-describing via the PHC string format, so a prefix check is usually enough:

PrefixAlgorithmAction on login
$argon2id$Argon2idRehash only if params below target
$2a$ / $2b$ / $2y$bcryptRehash to Argon2id (or raise cost)
$scrypt$scryptRehash to Argon2id
64 hex chars, no $Likely raw SHA-256Verify, then rehash immediately
32 hex chars, no $Likely raw MD5Verify, then rehash immediately

For extra safety in a mixed database, add an algo column (for example bcrypt10, argon2id_v1) rather than relying solely on string sniffing. It makes progress queryable: SELECT COUNT(*) FROM users WHERE algo != 'argon2id_v1' tells you exactly how many accounts remain on the legacy scheme.

A Zero-Downtime Migration Strategy

Rehash-on-login is lazy by design: it only touches accounts that actually authenticate. Combine it with a defined timeline so dormant accounts do not linger forever on weak hashes.

PhaseDurationWhat happens
1. Dual-verify deployDay 0Ship code that verifies both algorithms and rehashes active users to Argon2id
2. Passive migrationMonths 1–6Active users upgrade silently on each login; monitor the legacy count
3. Forced resetMonth 6–12Expire remaining legacy hashes; require a reset for the long tail of dormant accounts
4. Remove legacy codeAfter cutoverDelete the bcrypt/SHA-256 verification branch once zero legacy hashes remain

Most consumer applications see 80–90% of active accounts migrate within the first month simply because that is how many users log in during a monthly cycle. The forced-reset step in phase 3 mops up the inactive remainder. Encourage users to pair a strong, unique password with a manager such as NordPass during the reset flow, so the newly Argon2id-hashed credential is also high-entropy.

Common Pitfalls

  • Rehashing before verification. Always confirm the password matches first. Overwriting a hash from a failed login destroys the account.
  • Ignoring bcrypt's 72-byte truncation. bcrypt silently discards input past 72 bytes. When you rehash with Argon2id, hash the full plaintext the user submitted — the new hash covers the entire password, which is an upgrade.
  • Non-atomic writes. Wrap the hash update in a transaction or use an UPDATE ... WHERE id = ? that cannot partially apply. A crash mid-write must never leave a truncated hash.
  • Forgetting timing safety. Use the library's constant-time verifier; never compare hash strings with ==.
  • Skipping the dormant tail. Rehash-on-login never reaches accounts that do not log in. Without phase 3, those weak hashes stay forever.
  • Under-provisioning CPU. Argon2id at 64 MiB uses real memory and CPU per login. Load-test your auth endpoint and tune timeCost/memoryCost so a login burst does not exhaust the pool.

FAQs

What is password rehashing on login?

It is a migration technique that re-computes a stored password hash with a stronger algorithm or higher work factor at the moment a user successfully authenticates. Login is the only time the plaintext is available, so it is the only time you can transparently upgrade a hash without forcing a password reset.

Do I need to force a password reset to migrate to Argon2id?

No. Rehashing on login migrates silently. When a user logs in and their stored hash uses the old algorithm or outdated parameters, you verify the password, then immediately re-hash the plaintext with Argon2id and overwrite the stored value. Users never notice, and no reset email goes out.

How do I rehash users who never log in?

Rehash-on-login only upgrades active users; dormant accounts keep their old hash indefinitely. After a 6–12 month migration window, force a password reset for the remaining stale accounts or expire them. Track progress with an algorithm-identifier column so you can query how many rows still use the legacy scheme.

Can I rehash a SHA-256 or MD5 password on login?

Yes. Verify the password against the legacy digest first. If it matches, immediately re-hash the plaintext with Argon2id and replace the stored value. This is the fastest safe way to retire fast general-purpose hashes without a mass reset.

Does bcrypt's 72-byte limit affect migration?

It can. bcrypt truncates input at 72 bytes, so a long password may have been stored using only its first 72 bytes. When you rehash with Argon2id, hash the full plaintext the user just submitted — the new hash covers the entire password, a security improvement rather than a regression.

Conclusion

Rehash-on-login is the standard, zero-downtime way to modernize password storage. You deploy dual verification, let active users upgrade themselves to Argon2id one authentication at a time, and force-reset the dormant tail after a defined window. The libraries do the heavy lifting: needsRehash in Node, check_needs_rehash() in Python, and bcrypt.Cost plus a PHC decoder in Go. Verify first, rehash second, persist atomically, and you can retire bcrypt and SHA-256 without a single support ticket. For the parameters to target, revisit our Argon2 vs bcrypt guide and salted hashing implementation guide, and use our CSPRNG-secure password generator to seed every new credential with maximum entropy.

Generate a Free Strong Password →

More Password Security Tools

🔑 SecureKeyGen⚔️ TitanPasswords🛡️ Best Password Generator🔐 Free Strong Password⚡ Instant Password🗝️ Iron Vault Keys👨‍👩‍👧‍👦 Safe Pass Builder🛡️ Trusty Password⚙️ StrongPassFactory🔑 SecureKeyGen.org📚 TrustyPassword.org
We use cookies to improve your experience. Learn more