How do I migrate existing users to a new login system without making them sign up again?
Short answer
Move the accounts, not the people. Bulk-import users with their password hashes if the new system can verify your hash format, or migrate each person at their next login by checking the old password once. You can also skip the migration: keep your accounts and let signed-in users link the new login. loginos takes that last path, so people keep their account and connect loginos once.
You are moving sign-in from your own password table, or from one provider, to a new login system. If everyone has to sign up again, you lose people: some miss the email, some forget they had an account, and some don’t bother. The goal is that people sign in as usual and land in the same account, with the same data and the same user id.
Three techniques get you there, and they combine well: import the password hashes, migrate each person at their next login, or skip the migration and link accounts instead.
What are the ways to migrate users?
The three ways to migrate users differ in who moves, when, and what happens to passwords.
| Technique | Who moves | When | Passwords |
|---|---|---|---|
| Bulk import with hashes | Everyone | On one cutover day | Keep working, if the new system can check your hashes |
| Lazy migration | People who sign in | At each person’s next login | Keep working: checked once against the old system |
| Account linking | People who link | Whenever they choose | The old login works until you retire it |
Bulk import is the fastest and the most brittle. Lazy migration is the gentlest, but the old system has to keep running. Linking moves nothing: your database stays the source of truth, and the new login is attached to it.
How does bulk import with password hashes work?
A password hash is the output of a one-way function that you store instead of the password. Nobody can turn it back into the password, so the new system must run the same algorithm, with the same salt and parameters, to check a login. If it can’t, the hash is useless there, and the person has to reset their password.
Export everything needed to verify each hash: the algorithm, the salt, the cost or iteration count, and any secret key. Then check what the new system accepts. Support varies by product, and even by parameter:
| Provider | Hashes it can import | Watch out for |
|---|---|---|
| Auth0 | Argon2, bcrypt, scrypt, PBKDF2, HMAC, LDAP, MD4, MD5, SHA-1, SHA-256, SHA-512 | The plain password_hash field takes only bcrypt with 10 rounds; other formats go in custom_password_hash. Import files are limited to 500 KB each |
| Amazon Cognito | bcrypt, scrypt, Argon2id, PBKDF2-SHA256, since July 2026 | Caps such as bcrypt cost 12 and 600,000 PBKDF2 iterations. One algorithm per import job, and not every user pool has the feature yet |
| Firebase Authentication | bcrypt, standard scrypt, Firebase’s own scrypt, PBKDF2, HMAC, MD5, SHA | Argon2 import only through the Java Admin SDK |
| Clerk | bcrypt, argon2i, argon2id, PBKDF2, Firebase’s scrypt, phpass and others | Weak formats like MD5 and SHA-256 are rehashed to bcrypt at the person’s first sign-in |
Two details catch teams out. First, a supported algorithm can still exceed a limit: Django 5.2 hashes with 1,000,000 PBKDF2 iterations by default, above Cognito’s cap of 600,000. Second, scrypt comes in variants. Firebase stores passwords with a modified scrypt that needs the project’s signer key and other parameters, so a system that only knows standard scrypt can’t check those hashes.
Import a few test accounts whose passwords you know before you move everyone. If a format doesn’t fit, you can rehash passwords with compatible settings in the old system as people sign in, before the export. Accounts you still can’t carry over can be imported without a password: their owners reset it at the first sign-in, which keeps the account but adds a step.
How does lazy migration work?
Lazy migration, also called trickle migration, moves each person at their first sign-in on the new system. The old database stays online, read-only, until the migration ends.
- The person signs in on the new login page with their usual email and password.
- The new system doesn’t know them yet, so it calls a hook you write.
- Your hook checks the password against the old database, with the old algorithm.
- If it matches, the new system creates the account, hashes the password its own way, and signs the person in. The next sign-in no longer needs the hook.
Hosted providers offer lazy migration as a feature. In Auth0, it is a custom database connection with Import Users to Auth0 turned on. Amazon Cognito has a migrate user Lambda trigger, which runs for password sign-in flows like USER_PASSWORD_AUTH, but not for SRP (Secure Remote Password) sign-in.
Lazy migration avoids hash compatibility problems, and accounts nobody uses never move. It has two costs. Your hook sees plain-text passwords, so it needs the same care as a login form. And people who don’t come back are never migrated.
How do you handle people who never come back?
Every migration leaves a long tail: accounts whose owners don’t sign in during your migration window. Plan for them before you start.
- Tell people early. Announce the change by email and in the app: the date, what changes, and what they need to do, which is often nothing.
- Pick the window from your own data. Look at how often your active users return, and keep the old path open long enough to cover them.
- Keep a way back in. After the window, a password reset by email gets anyone into the new system with their old account.
- Import the rest without passwords. At the end of a lazy migration, import the accounts that didn’t move, so a reset finds them.
- Delete what you no longer need. Remove the old password hashes once nothing reads them, and handle long-inactive accounts as your data retention policy says.
How loginos does it
loginos doesn’t import password hashes, and it has no migration hook. Its path is the third technique: linking. People keep their account with you and connect a loginos account to it once, from your settings page. Your users table stays the source of truth, and nothing moves.
When a signed-in user clicks “Link loginos”, your server pushes the request to loginos with your own id for that user:
curl -X POST https://loginos.io/oauth/par \
-u "$LOGINOS_APP_KEY:$LOGINOS_APP_SECRET" \
-d response_type=code \
-d redirect_uri=https://app.example.com/api/auth/login/single/callback \
-d "scope=openid profile email" \
-d state=RANDOM_STATE \
-d code_challenge=BASE64URL_SHA256_OF_VERIFIER \
-d code_challenge_method=S256 \
-d external_id=u_1842 \
-d external_label=jane@example.comThe person signs in to loginos, or creates an account, sees both accounts and approves. When your server redeems the code, loginos makes the link, and every token from then on carries "external_id": "u_1842". Link existing accounts has the complete flow, and the link flow explained shows why each step is there.
With linking, the long tail looks different. People who haven’t linked keep signing in with their password, so nobody is locked out on a cutover day. The console’s Overview counts linked accounts, and GET /api/v1/users?external_id=u_1842 from the Management API tells you whether a given user has linked. You decide when, or whether, to retire passwords.
loginos isn’t the right fit if everyone must move with their current passwords on one date, or if your password table has to go now. A provider with hash import or a migration hook suits that better.
Frequently asked questions
Do users have to reset their passwords after a migration?
Only when the new system can’t verify their old password hash and you don’t use lazy migration. With a supported hash format, or a hook that checks the old password at the next login, people keep their password. With account linking, nothing is reset: people keep their old login and add the new one.
How do I find out which hashing algorithm my app uses?
Check your framework’s password settings, or the start of a stored hash. Hashes that start with $2a$, $2b$ or $2y$ are bcrypt, and $argon2id$ is Argon2id. Django’s default looks like pbkdf2_sha256$<iterations>$<salt>$<hash>. Note the cost or iteration count too, because the new system has to accept it.
Should I use bulk import or lazy migration?
Use bulk import when the new system accepts your hash format and parameters, and you want one cutover date. Use lazy migration when it doesn’t, or when you want inactive accounts to stay behind. You can combine them: migrate lazily for a few months, then import the rest without passwords.
Can I migrate users away from Firebase Authentication?
Yes, but plan for its hashes. Firebase’s default password hash is a modified scrypt, and checking it needs the project’s hash parameters, which the Firebase console shows under Authentication. Some systems accept these hashes as they are: Clerk has a scrypt_firebase hasher. For a target that doesn’t, use lazy migration or account linking.
One account for every domain you run
loginos is an OpenID Connect provider with passkeys, a user pool shared by all of your domains, roles in every token, and account linking for the users you already have. Add your platform and its domains in a few minutes.
Keep reading
- Existing usersHow do I add SSO to an app that already has users?Add SSO as a second way into the accounts you already have, not as a new user base. Let signed-in users connect the new login from their settings, so your app stores the provider’s user id against their account. Keep passwords working while people link, then decide whether to retire them. Never match accounts by an unverified email. With loginos, every token then carries your own user id.7 min read
- Existing usersHow do I link existing user accounts to an OAuth login?Keep a table that maps each provider’s user id, the sub claim, to your own user id. Start the link from a page where the person is already signed in, send state and PKCE, and save the link on your server only after you exchange the code. Refuse a provider account that is already linked to someone else, and never link accounts by email.7 min read
- Existing usersHow can users connect their existing account to a new login provider?Give signed-in users a Connect button in their account settings. It sends them to the provider to sign in and approve, then back to your settings page with a clear confirmation. Your server makes the link only after it checks state and exchanges the code, and it never matches accounts by email. Then show which account is connected, with a way to disconnect it.7 min read
- Choosing a providerWhat is a good alternative to Auth0?Good Auth0 alternatives include hosted services like Clerk, WorkOS AuthKit, Stytch, Descope and Kinde, and open-source servers you run yourself, like Keycloak, Zitadel, authentik, Ory and Logto. Firebase, Supabase and Amazon Cognito bundle sign-in with their platforms, and Auth.js or Better Auth keep it in your code. Choose by who runs it and how you would leave. loginos fits products on many domains.7 min read