close

DEV Community

authagonal
authagonal

Posted on • Originally published at authagonal.io

Importing users without a password reset

Every identity migration guide eventually reaches the same paragraph, and it's always a little apologetic: "users will need to reset their passwords." It gets treated like a law of nature. It isn't. It's a choice, usually forced by a tool that didn't want to do the harder thing.

The harder thing is verifying your users' existing password hashes in place, so they sign in after the move with exactly the credentials they had before and never notice anything happened. Whether you can do it comes down to one question: can you get the old hashes, and can the new system verify them?

Password hashes are more portable than people think

A password hash isn't a secret algorithm. bcrypt is bcrypt. A bcrypt hash carries its own cost factor and salt inside the string, so anything that implements bcrypt can verify a hash any other bcrypt system produced. The same is true of the PBKDF2 format ASP.NET Identity uses: documented, versioned, self-describing. If you know what you're holding, you can check a password against it without ever knowing the password.

So a migration that preserves logins doesn't need the plaintext (nobody has it) and doesn't need to re-hash everyone up front. It needs to obtain the stored hashes and verify against them on sign-in, upgrading each one to its own format quietly the first time a user logs in. That last part is lazy migration: carry the old hash, verify it once, replace it transparently. Over a few weeks of normal logins your user table re-hashes itself and the legacy formats age out, with zero resets and zero support tickets.

The dual-path bit

The wrinkle is that different sources hand you different formats, and a good importer verifies both:

  • From self-hosted Duende / ASP.NET Identity: the V3 PBKDF2 hashes (and any legacy bcrypt) verify natively and rehash on first sign-in. This is the easy case, because it's the same scheme the destination already uses. Most teams are surprised it's that clean.
  • From Auth0: bcrypt hashes verify verbatim. The catch isn't the format, it's getting them.

When you genuinely can't

Auth0's Management API never returns password hashes. That's a deliberate policy, not a gap in anyone's tooling, and you should be suspicious of any "one-click Auth0 export" that claims to include passwords without it. The supported path is a support-assisted bulk export: an NDJSON file with each user's bcrypt hash. Get that file and the hashes import verbatim, lazy migration and all, and the move is invisible.

If you can't wait on it, the fallback is the honest version of the apologetic paragraph: users set a new password on first sign-in. Nothing was lost, because there was no hash to carry. The difference is you chose it knowingly, rather than because the tool couldn't do better.

Why this matters more than it sounds

A forced reset is the single most visible, most alarming thing you can do to a user base mid-migration. It generates support load, it trains users to expect a phishing-shaped "click here to reset" email, and it's the moment a quiet infrastructure change becomes everyone's problem. Avoiding it is most of what makes a migration feel like nothing happened, which is how a migration should feel.

So before you accept "everyone resets," ask the two questions. For most moves the answer is yes on both, and the apologetic paragraph was never necessary.

See what an import would carry across — the preview is read-only and shows you everything before anything is written.

Top comments (0)