Skip to content
architecture

Password Hash Migration: The Technical Runbook

Updated 2026-09-12 · 14 min read · By @guptadeepak

Key takeaways

  • Import support and export support are different products. Almost every platform documents how to bring hashes in. Far fewer document how to take them out.
  • Lazy rehash on login is the default pattern: verify against the old algorithm, rehash with the new one inside the same successful login, retire the old column when the tail is small enough.
  • Bcrypt is the lowest common denominator. If your hashes are in a format the destination cannot verify, a custom verification hook is the escape hatch.
  • MFA enrollments usually do not migrate. TOTP secrets are exportable in principle and withheld in practice, and SMS enrollments are vendor-side state.
  • Passkeys survive a migration only if the RP ID does not change and the destination will accept imported credential public keys.
  • In B2B, every SAML connection and SCIM token has to be re-established by the customer's own IT admin, which makes migration a customer program rather than a cutover.

The asymmetry that decides everything

A password hash is not a secret you can regenerate. It is the only artifact that lets an existing user log in without being interrupted, and interruption is expensive: a forced reset on a consumer base reliably loses a percentage of dormant users permanently. That loss is the real cost of a migration, and it is entirely determined by whether hashes come with you.

Establish what you can actually get out

Before any planning, answer three questions about your current platform in writing. What hash algorithm and parameters are in use, in what encoding. Is there a documented export path for the hashes, an undocumented one through support, or none at all. And what does the export contain beyond the hash: salt, work factor, algorithm marker, and the user identifier that ties it to the rest of the record.

The answers sort your current platform into three tiers.

You own the database. Keycloak, FusionAuth, Supabase, Authentik, Ory, SuperTokens, and anything self-hosted. The hash is a column. Export is a query, and the only real work is understanding the stored format, which is often a PHC string that encodes the algorithm and parameters inline.

Hosted, with a documented export. A handful of hosted vendors will produce a hash export, sometimes only through a support request and under a specific contractual clause. Read the clause. "Data export" in a standard agreement frequently means profile data and excludes credential material.

Hosted, with no export. The hashes stay. Your options are a forced reset for everyone, or a proxy period where the old platform keeps verifying logins while the new one captures a fresh hash. The second option is described below, and it is how competent teams leave a platform that will not release credentials.

What the destination will accept

Destination platforms converge on a similar list, and bcrypt is the safe intersection. Auth0's bulk import accepts Argon2, bcrypt, PBKDF2, scrypt, HMAC, LDAP RFC-2307 userPassword, and the MD and SHA families, with format constraints that matter. Its bcrypt path expects $2a$ or $2b$ at ten rounds, and its Argon2 path expects a PHC string with no separate salt field. Cognito's CSV import accepts bcrypt, scrypt, Argon2id, and PBKDF2 with SHA-256.

Two consequences follow.

Exotic formats need a translation step, not a rejection. A legacy sha1(md5(password) + salt) construction is not importable anywhere as-is. It is still migratable: wrap it in a peppered outer hash, or carry it through a verification hook.

Round-tripping is lossy. Importing a bcrypt hash into a platform that stores Argon2id internally does not upgrade it. The credential remains bcrypt-strength until the user next logs in and it is rehashed. That is fine, and it is worth knowing when a security review asks why your stated algorithm and your actual stored algorithm differ for part of the base. The algorithm comparison in password security and storage covers what each format is actually worth.

The lazy rehash pattern

Rehash on login is the standard mechanism for moving a user base without interrupting anyone, and its correctness depends on one detail: the rehash happens inside the same request that already proved the password. The destination stores the legacy hash and an algorithm marker, verifies against them once, replaces them with its own hash, and never looks at the legacy value again.

The sequence:

on login(email, password):
  user = load(email)

  if user.legacy_hash is present:
      if not verify_legacy(user.legacy_algo, user.legacy_hash, password):
          return failure          # do not fall through to the new hash
      user.password_hash = hash_current(password)
      user.legacy_hash = null
      user.legacy_algo = null
      save(user)
      return success

  return verify_current(user.password_hash, password)

Three rules make it safe.

Never verify against both and accept either. A user whose legacy hash verified must have the legacy column cleared in the same transaction. Leaving both populated creates two live credentials for one account, and the weaker one sets the security level.

Fail closed on an unknown legacy algorithm. A record whose algorithm marker is missing or unrecognized is a data error, not a login to allow through.

Count the migration. Emit an event on every rehash and track the proportion of the base still carrying a legacy hash. Without that counter you cannot decide when the forced-reset step is safe, and instrumentation of exactly this kind is the subject of the wider observability discipline these migrations expose.

For a destination that will not run your code at login, the same pattern exists as a vendor primitive. Okta's password import inline hook fires on the user's first sign-in and calls your service to verify the credential against the old store, after which Okta takes ownership of the password. Auth0's custom database with import mode does the same thing. Both are how you leave a platform that refuses to export.

What cannot migrate

Passwords are the part of a migration that can be made invisible. Everything else in the credential inventory usually cannot. Second factors, recovery codes, and passkeys are either sealed by the source platform, bound to an identifier that is about to change, or both. Set expectations on this early, because it is the part that generates support volume and the part a cutover plan tends to discover late.

SMS and voice MFA enrollments. These are vendor-side records tied to a delivery integration. They are re-enrolled at the destination. Given that SMS is on its way out anyway, a migration is a reasonable moment to move that population to a stronger factor rather than reproducing the old one.

TOTP shared secrets. Portable in principle, withheld in practice. Most hosted platforms treat the shared secret as sealed material that never leaves. Assume re-enrollment, communicate it ahead of the cutover, and keep a grace period where a user can re-enroll without a support ticket.

Recovery codes. Regenerate them. They are hashed at rest in any competent implementation, which means they are subject to the same export problem as passwords with none of the benefit of carrying them over.

Passkeys, unless two conditions hold. This is the one most migration plans get wrong. A passkey is scoped to the relying party identifier it was created against. If your login lives on accounts.example.com today and the new vendor hosts it on example.vendor.com, every existing passkey is invalid regardless of what either platform exports. Where the RP ID is preserved, the credential record itself is portable in principle: credential ID, public key, signature counter, transports, and AAGUID are ordinary data. Whether the destination will accept an import of those fields is a direct question to ask, and many will not.

Sessions. Everyone is logged out at cutover. Say so in the comms.

The B2B coordination problem

In B2C, migration is something you do to your own database on your own schedule. In B2B, the credential is often not the hard part at all, because federated users have no password with you. The hard part is that every enterprise customer's identity configuration is owned by that customer's IT administrator.

For each enterprise tenant, someone on the customer side has to update the SAML or OIDC connection to point at new endpoints and a new entity ID, re-establish trust with a new certificate, re-issue a SCIM bearer token against a new base URL, re-map attributes if the destination names them differently, and re-test provisioning and deprovisioning. See SCIM provisioning and enterprise SSO for what each of those steps involves.

None of that happens on your timeline. It happens on their change calendar, which for a regulated customer may be a quarterly window. Plan accordingly:

Do

  • Sequence tenants by risk and size

    Start with the smallest and friendliest customers, and keep both platforms live through the overlap.

    The first three tenants surface every mapping bug the sandbox missed.

  • Give each admin a filled-in runbook

    Send the customer's own entity ID, endpoints, and token, not a link to generic documentation.

    Generic docs produce support tickets; pre-filled values produce completed migrations.

  • Dual-accept provisioning during the overlap

    A tenant that has not cut over yet must keep provisioning against the old endpoint without special handling.

    Enterprise change calendars run on quarters, not on your release schedule.

  • Treat a stalled tenant as a project

    Assign an owner and a date, because a connection nobody owns never gets re-established.

    The long tail of a B2B migration is measured in months, not weeks.

Don't

  • Announce one global cutover date

    Federated tenants cut over individually, on their own administrators' schedules.

    A single date turns every late customer into an outage you caused.

  • Assume the customer remembers the original configuration

    The admin who set up the SAML connection has usually moved on, and the settings are undocumented on their side.

    Provide current values and expected values side by side.

  • Deprovision the old connection early

    Keep it until you have seen a successful login and a successful SCIM sync on the new one.

    Rollback is only possible while the old path still works.

  • Let the overlap run open-ended

    Two live identity systems means twice the attack surface and twice the audit scope.

    Set the end date when the overlap begins.

Pre-migration checklist

Work through this before writing any code. Each item has sunk a migration that skipped it.

  1. Get the export answer in writing. Algorithm, parameters, encoding, delivery mechanism, and the contract clause that entitles you to it.
  2. Verify one hash end to end. Take a single exported hash, import it into a sandbox on the destination, and log in as that user. Do this in week one, not week six.
  3. Inventory the credential types you hold. Passwords, TOTP, SMS, passkeys, recovery codes, social identities, and federated connections. Each has a different migration path and some have none.
  4. Confirm the RP ID. If it changes, passkeys are gone and your passwordless rollout restarts.
  5. Map social and federated identities by provider subject. A Google-linked account keys on the sub claim, and a destination that keys on email address will silently merge or split accounts.
  6. Decide the forced-reset date up front. Announce the lazy window with an end, or it will never end.
  7. Plan the rollback. Keep the source system readable, not just running, until the counter of unmigrated accounts is small enough to accept.
  8. Rehearse on a copy. A full-volume dry run against a sandbox is the only way to discover the encoding problem that affects 4 percent of your rows.
  9. Write the user communication before the cutover, not during. The re-enrollment message is the one users will actually read.
  10. Instrument the tail. Migrated versus unmigrated counts, rehash events per day, failed legacy verifications, and support contacts tagged to the migration.

The broader sequencing, vendor selection, and rollback strategy sit in the CIAM migration framework. This runbook is the credential layer underneath it, and it is the layer that decides whether your users notice.

Related vendors

Where to next

FAQ

Can you export password hashes from a CIAM provider?
Sometimes, and it depends entirely on the provider rather than on the format. Self-hosted platforms such as Keycloak, FusionAuth, and Supabase keep hashes in a database you control, so export is a query. Hosted platforms vary: some will produce a hash export on request or through a support process, and others never release hashes at all. Amazon Cognito is the clearest example of the asymmetry, because it added password-hash import in July 2026 while still offering no path to export the hashes it holds. Confirm the export path in writing before signing, not at migration time.
What is lazy migration or rehash on login?
A pattern where users are moved without ever being asked to reset a password. The destination system stores the legacy hash alongside a marker for its algorithm. On the user's first login, it verifies the submitted password against the legacy hash, and if that succeeds, it immediately computes a new hash with its own algorithm, writes that, and clears the legacy value. The user notices nothing. After a defined window the remaining unmigrated accounts are handled by a forced reset, because the tail of a login-driven migration never reaches zero.
Can you migrate passkeys between CIAM vendors?
Only under two conditions. The relying party identifier has to stay the same, because a passkey is cryptographically scoped to the RP ID it was created against, and a move from your own domain to a vendor-hosted login domain invalidates every credential. The destination also has to accept imported WebAuthn credential records, which means credential ID, public key, sign count, and transports. The FIDO Credential Exchange specifications address portability between credential managers, which is a different problem from relying-party migration.
Do MFA enrollments survive a CIAM migration?
Usually not. SMS and voice enrollments are vendor-side state tied to a delivery integration, so they are re-enrolled at the destination. TOTP shared secrets are technically portable and rarely exported, because most hosted platforms treat them as sealed material. Plan for a re-enrollment campaign with a grace period, and expect it to be the most visible part of the migration to end users.
How long should you run dual-algorithm verification?
Long enough to catch your seasonal users, which for most consumer products means one to two login cycles of your least active cohort. Ninety days captures the bulk of an active base. Anything beyond about six months is carrying two verification paths for a shrinking population, and at that point a forced reset on the remainder is cheaper and safer than keeping a legacy algorithm alive in production.
What makes B2B migrations harder than B2C?
The coordination surface. A consumer migration is something you do to your own database. A B2B migration requires every enterprise customer's IT administrator to re-establish a SAML or OIDC connection, re-issue a SCIM bearer token, and re-test provisioning against the new endpoint, on their change calendar rather than yours. The technical work is smaller than the consumer case and the elapsed time is longer.

Sources

  • Auth0 Docs, User Data JSON Schema for Bulk User Imports (supported hash algorithms)
  • AWS, Amazon Cognito now supports importing users with password hashes (July 2026)
  • Okta Developer, Password import inline hook
  • OWASP Password Storage Cheat Sheet
  • FIDO Alliance, Credential Exchange Specifications (CXF / CXP)
Last reviewed 2026-09-12.