Migrate users from Auth0 to another CIAM without forcing password resets
Export Auth0 profiles, get the bcrypt password hashes through support, then move users with a bulk hash import, a lazy migration, or both, without breaking the sub claim your apps depend on.
- Auth0 tenant admin access and a Management API token with read:users
- A paid Auth0 plan if you want password hashes exported
- A target CIAM tenant that accepts bcrypt hashes or a sign-in migration hook
- A list of every app and API that stores the Auth0 user_id or token sub
Moving off Auth0 without a mass password reset takes four things: export the profiles with the Management API, get the bcrypt password hashes through an Auth0 support ticket, load users into the target with those hashes or migrate them lazily at sign-in, and map every old auth0|... user ID to the new one before tokens with a different sub reach your APIs. When you finish, the target CIAM issues all tokens, Auth0 is read-only, and users noticed nothing more than a new login page.
Before you start: pick a strategy
| Strategy | How passwords move | Pick it when | Main cost |
|---|---|---|---|
| Bulk import with hashes | Target imports Auth0 bcrypt hashes | You can wait for Auth0 support and the target accepts bcrypt | Ticket lead time, a one-time sensitive file |
| Lazy (just-in-time) | Target checks the password against Auth0 at first sign-in | You want to start now, or the Free plan rules out a hash export | Auth0 stays live until the long tail signs in |
| Hybrid | Lazy first, bulk import for whoever is left | Large user bases with many dormant accounts | Two code paths for a few weeks |
If you are still choosing a target, start with how to choose a CIAM platform.
1. Export the profiles from Auth0
Create an export job with POST /api/v2/jobs/users-exports (scope read:users). Use format: "json" so app_metadata and user_metadata come out whole; CSV exports are limited to 30 fields and cannot include those objects as single fields. Omit connection_id to export every connection.
curl -s -X POST "https://$AUTH0_DOMAIN/api/v2/jobs/users-exports" \
-H "Authorization: Bearer $MGMT_TOKEN" -H "Content-Type: application/json" \
-d '{"connection_id":"con_XXXXXXXXXXXXXXXX","format":"json",
"fields":[{"name":"user_id"},{"name":"email"},{"name":"email_verified"},
{"name":"name"},{"name":"created_at"},{"name":"last_login"},
{"name":"app_metadata"},{"name":"user_metadata"}]}'
# poll until status is "completed", then download the "location" URL
curl -s "https://$AUTH0_DOMAIN/api/v2/jobs/$JOB_ID" -H "Authorization: Bearer $MGMT_TOKEN"
The file is NDJSON, one user per line.
Check: line count matches the user count per connection in the dashboard. Common failure: the download fails because the link is valid for 60 seconds; call the job endpoint again for a fresh one (job data is deleted after 24 hours). Rollback: nothing to undo; delete local copies of the file when done.
2. Request password hashes and MFA secrets
The export never contains password hashes. Auth0 exports them on request through a support ticket, and not on the Free plan. Per the hash export procedure, you supply an RSA 4096-bit PGP public key, a CISO, CSO, or VP-level signer signs the acknowledgment form, another tenant admin confirms, and Auth0 reviews eligibility with no ETA. The download link expires after 3 days.
Auth0 hashes database passwords with bcrypt. Social and enterprise users (Google, SAML, OIDC) have no Auth0 password: recreate those connections in the target and link users by the provider ID in each user's identities array. The docs do not list which MFA factor types the export covers, so ask in the ticket and plan for re-enrollment of anything the target cannot import.
Check: gpg --decrypt works and the hashes start with $2a$ or $2b$.
Common failure: Auth0 rejects the key because it is ECC or EdDSA; only RSA keys pass validation.
Rollback: destroy the decrypted file after import; it is a credential store.
3. Map user IDs before anything else
Auth0 uses the prefixed user_id as the sub claim, for example auth0|65a1... or google-oauth2|1035.... The target issues its own sub, and some targets cannot reuse yours: FusionAuth's Auth0 migration guide notes that IDs must be UUIDs and stores the Auth0 ID in user.data instead.
Store the old ID on every imported user (a custom attribute such as custom:auth0_id in Cognito) and either emit it as a custom claim or keep a lookup table your APIs query. Then update every service that keys rows on sub.
Check: a test user's new token carries the old ID, and your API resolves it to the same account. Common failure: duplicate accounts because one person had both a password and a Google identity in Auth0; merge on verified email before import. Rollback: the mapping is additive, so leave it in place.
4. Bulk import with hashes
Amazon Cognito imports bcrypt hashes from a CSV with a password_hash column and creates those users as CONFIRMED:
aws cognito-idp create-user-import-job --job-name auth0-batch-1 \
--user-pool-id "$POOL_ID" --cloud-watch-logs-role-arn "$ROLE_ARN" \
--password-hashing-algorithm BCRYPT
curl -T users.csv -H "x-amz-server-side-encryption:aws:kms" "$PRE_SIGNED_URL"
aws cognito-idp start-user-import-job --user-pool-id "$POOL_ID" --job-id "$JOB_ID"
Cognito caps bcrypt cost at 12, files at 500,000 rows, and those users cannot use SRP until their first sign-in. FusionAuth imports the same hashes through POST /api/user/import, and Okta supports creating users with an imported hashed password.
Check: the CloudWatch log shows [SUCCEEDED] per line, and a test user signs in with the old password.
Common failure: "Parameter out of bounds" for users whose hashes were created with a higher cost factor; move them to the lazy path.
Rollback: delete the imported users and rerun; Auth0 is untouched.
5. Lazy migration at sign-in
The target calls your code when an unknown user signs in, and your code checks the password against Auth0 with the password-realm grant. Enable the Password grant type on a dedicated Auth0 application first.
// Cognito migrate user trigger (UserMigration_Authentication)
const r = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'http://auth0.com/oauth/grant-type/password-realm',
realm: 'Username-Password-Authentication',
username: event.userName, password: event.request.password,
client_id: MIGRATION_CLIENT_ID, client_secret: MIGRATION_CLIENT_SECRET,
scope: 'openid email',
}),
});
if (!r.ok) throw new Error('Bad password');
event.response.userAttributes = { email: event.userName, email_verified: 'true',
'custom:auth0_id': auth0UserIdFromIdToken(await r.json()) };
event.response.finalUserStatus = 'CONFIRMED';
event.response.messageAction = 'SUPPRESS';
return event;
The Cognito trigger fires only for USER_PASSWORD_AUTH, ADMIN_USER_PASSWORD_AUTH, or USER_AUTH with a password, never for SRP. Okta's equivalent is the password import inline hook. If the target is another Auth0 tenant, use a custom database connection with Import Users to Auth0 enabled and login and getUser scripts.
Check: first sign-in creates the user in the target; the second sign-in makes no call to Auth0. Common failure: the trigger never fires because the app client only allows SRP. Auth0 also warns that some attack protection features may fail with this grant, so rate-limit the hook yourself. Rollback: detach the trigger; unmigrated users keep signing in at Auth0.
6. Cut over, sweep, and keep a way back
Run lazy migration for a fixed window (a planning estimate: four to eight weeks), then bulk import everyone left from a fresh export. Point apps at the target with a feature flag per app, not a big-bang DNS switch. Keep the Auth0 tenant read-only until no migration call has reached it for a full billing cycle.
Check: Auth0 logs show zero password-realm calls from the migration client. Common failure: users who changed their password in the target, then were rolled back to Auth0, sign in with the stale password. Rollback: flip the per-app flag back to Auth0; for users created after cutover, trigger a password reset because Auth0 never saw their passwords.
Verify it works
- Sign in to the target as a migrated user with the old password: no reset prompt, and the ID token carries the old Auth0 ID claim.
- Sign in with a Google account that existed in Auth0: the target links it to the same profile, not a new one.
- Call one downstream API with a new token and confirm it returns the same user's data as before.
aws cognito-idp admin-get-user --user-pool-id "$POOL_ID" --username alice@example.comreturnsUserStatus: CONFIRMED.- Compare target user count against the final Auth0 export; the difference should be only accounts you chose to drop.
Related
Frequently asked questions
- Does Auth0 export password hashes?
- Not through the Management API or the dashboard export. Auth0 support exports password hashes and MFA secrets as a PGP-encrypted file on request; the option is not available on the Free plan, an executive must sign an acknowledgment form, and Auth0 gives no ETA.
- What hash algorithm does Auth0 use for passwords?
- Auth0 database connections hash passwords with bcrypt. Targets that accept bcrypt hashes, such as Amazon Cognito user import with the BCRYPT algorithm or FusionAuth's user import API, can take them as they are.
- How do I migrate from Auth0 without a password reset?
- Either import the exported bcrypt hashes into the new CIAM, or run a lazy migration where the new CIAM checks each user's password against Auth0 at first sign-in and then stores it natively. Most teams run lazy migration first and bulk import the remaining users at the end.
- What happens to the user ID in a CIAM migration?
- Auth0 user IDs look like auth0|abc123 and are used as the sub claim in tokens. The new CIAM will issue a different sub, so every app or API that keys data on sub needs a mapping table or a custom claim carrying the old Auth0 ID.
- Do social login users need password migration?
- No. Users from Google, Apple, or enterprise SAML and OIDC connections have no Auth0 password. Configure the same upstream connection in the target and link users by the provider's user ID from the Auth0 identities array.
Related on Start with Identity
- ArticleHow to Migrate Your CIAM Platform Without Downtime
A practical playbook for moving from a legacy customer identity platform to a modern one without locking users out: user and password migration strategies, phas
- BlogPassword manager mSecure tells users to change passwords and remove passkeys, without saying what happened
mSecure's developer emailed current and former customers on November 3 about suspicious activity linked to the password manager, advising them to change passwor
- RankingBest CIAM for High Scale: Top 5 Platforms for Millions of Users
The best CIAM platforms for high scale in 2026: Auth0, Ping Identity, MojoAuth, Stytch, and Transmit Security. Ranked for throughput, global reach, spike handli
- BlogChainalysis now says password hashes and live tokens for all users were exposed
Chainalysis said on November 4 that names, usernames, password hashes and live tokens for all users were exposed, reversing its October 30 statement that no ses
- CVECheck Point Remote Access VPN lets attackers in without a password over IKEv1
A certificate validation logic flaw in Check Point Remote Access and Mobile Access over deprecated IKEv1 let attackers open a VPN session without a valid passwo
- GuideFrom RBAC to ReBAC: when and how to migrate
Migrate from RBAC to ReBAC when access depends on *which object* a user is related to, not just *what role* they hold. If you are creating roles like `editor_fo