# Migrate users from Auth0 to another CIAM without forcing password resets

Source: https://startwithidentity.com/recipes/migrate-users-from-auth0/
Last updated: 2026-11-05
License: content by Start with Identity. Cite the source URL.

---

Moving off [Auth0](https://startwithidentity.com/vendors/ciam/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](https://startwithidentity.com/glossary/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](https://startwithidentity.com/guides/buyer-guides/how-to-choose-a-ciam-platform/).

## 1. Export the profiles from Auth0

Create an export job with [`POST /api/v2/jobs/users-exports`](https://auth0.com/docs/api/management/v2/jobs/post-users-exports) (scope `read:users`). Use `format: "json"` so `app_metadata` and `user_metadata` come out whole; [CSV exports](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports) are limited to 30 fields and cannot include those objects as single fields. Omit `connection_id` to export every connection.

```bash
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](https://auth0.com/docs/troubleshoot/customer-support/manage-subscriptions/export-data) through a support ticket, and not on the Free plan. Per the [hash export procedure](https://auth0.com/docs/manage-users/user-migration/export-password-hashes-and-mfa-secrets), 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](https://auth0.com/docs/manage-users/user-migration/bulk-user-import-database-schema-and-examples). 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](https://auth0.com/docs/authenticate/database-connections/custom-user-id), 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](https://fusionauth.io/docs/lifecycle/migrate-users/provider-specific/auth0) 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](https://startwithidentity.com/vendors/ciam/amazon-cognito/) [imports bcrypt hashes](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-using-import-tool.html) from a CSV with a `password_hash` column and creates those users as `CONFIRMED`:

```bash
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](https://startwithidentity.com/vendors/ciam/fusionauth/) imports the same hashes through `POST /api/user/import`, and [Okta](https://startwithidentity.com/vendors/iam/okta/) supports [creating users with an imported hashed password](https://developer.okta.com/docs/reference/api/users/).

**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](https://auth0.com/docs/get-started/authentication-and-authorization-flow/resource-owner-password-flow/call-your-api-using-resource-owner-password-flow). Enable the Password grant type on a dedicated Auth0 application first.

```js
// 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](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-migrate-user.html) 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.com` returns `UserStatus: CONFIRMED`.
- Compare target user count against the final Auth0 export; the difference should be only accounts you chose to drop.

## Related

- [CIAM](https://startwithidentity.com/glossary/ciam/)
- [How to choose a CIAM platform](https://startwithidentity.com/guides/buyer-guides/how-to-choose-a-ciam-platform/)
- [CIAM greenfield implementation](https://startwithidentity.com/guides/implementation/ciam-greenfield/)
- [Validate a JWT](https://startwithidentity.com/recipes/validate-a-jwt/)
- [Credential stuffing](https://startwithidentity.com/techniques/credential-stuffing/)
