Import users into Auth0
Ce contenu n’est pas encore disponible dans votre langue.
This guide walks through a complete import into Auth0: preparing the tenant, rehearsing with synthetic users that cover every password hash algorithm and MFA factor type you’re likely to meet, validating, importing, verifying logins, and handling the users Auth0 can’t take as-is.
The rehearsal uses iamigrate testdata generate, so you can run the whole flow against a development tenant before touching real data. Once it works end to end, swap the fixture for your own CMF export (see Step 9).
How iamigrate imports into Auth0
Section titled “How iamigrate imports into Auth0”iamigrate import auth0 uses Auth0’s bulk user import job (POST /jobs/users-imports). For each CMF user it:
- Translates the password hash into Auth0’s
password_hashorcustom_password_hashformat. - Translates portable MFA enrollments into
mfa_factors, so users don’t have to enroll again. - Batches users into chunks that stay under Auth0’s 500KB per-job limit, then submits each chunk and polls it until it finishes.
- Collects per-user results, plus any follow-up actions users need, in
import-report.json.
If organizations.cmf.jsonl or roles.cmf.jsonl sit next to the users file, a second phase creates the organizations and roles, then assigns memberships.
Prerequisites
Section titled “Prerequisites”- iamigrate installed (see Installation).
- An Auth0 tenant. Use a dedicated development tenant for the rehearsal. Imported users stay in it until you delete them.
- Dashboard access with the Admin or Editor - Users role.
1. Prepare the Auth0 tenant
Section titled “1. Prepare the Auth0 tenant”Create or choose a database connection
Section titled “Create or choose a database connection”Users are imported into a single database connection. In the Auth0 Dashboard, go to Authentication → Database and either create a connection or open an existing one. The connection can use the Auth0 user store, or a custom database with Import Users to Auth0 turned on.
Note the connection name, or copy its identifier (it looks like con_XXXXXXXXXXXXXXXX). You can pass the name as --connection or the identifier as --connection-id. If the tenant has only one database connection, you can leave out both and iamigrate uses that one. Looking up a connection by name, or automatically, needs the read:connections scope on your token. --connection-id works without it.
Enable the MFA factors you import
Section titled “Enable the MFA factors you import”Imported totp, sms, and email enrollments only work if the matching factor is enabled on the tenant. Under Security → Multi-factor Auth, turn on the factors your users have:
| CMF factor | Auth0 factor to enable |
|---|---|
totp | One-time Password |
sms | Phone Message (SMS and/or voice) |
email |
Create a Management API token
Section titled “Create a Management API token”Create a Machine-to-Machine application authorized for the Auth0 Management API, and grant it these scopes:
| Scope | Needed for |
|---|---|
create:users, read:users | Bulk import jobs, job status and errors, diff auth0 |
read:connections | Finding the database connection by name (--connection) or automatically (not needed with --connection-id) |
update:users | Assigning global roles to users |
read:roles, create:roles | Roles phase (only if you import roles.cmf.jsonl) |
read:organizations, create:organizations, create:organization_members, create:organization_member_roles | Organizations phase (only if you import organizations.cmf.jsonl) |
Then export the tenant domain and the application’s credentials. iamigrate exchanges them for a Management API token through the client_credentials grant, and renews the token before it expires, so long imports don’t fail midway:
export AUTH0_DOMAIN=your-tenant.us.auth0.comexport AUTH0_CLIENT_ID=... # the Machine-to-Machine application's Client IDexport AUTH0_CLIENT_SECRET=... # and its Client SecretIf you’d rather not hand iamigrate the client secret, export a Management API access token instead:
export AUTH0_TOKEN=eyJhbGciOi... # Management API access tokenManagement API tokens expire (after 24 hours by default) and iamigrate can’t renew them. For large imports, get a fresh token right before you start.
2. Generate a representative test export
Section titled “2. Generate a representative test export”Generate a fixture that mixes the password hash algorithms and MFA setups you’ll find in most real user bases:
iamigrate testdata generate \ --count 200 \ --hash bcrypt:cost=10 \ --hash bcrypt:cost=12 \ --hash scrypt:cost=16384,blockSize=8,parallelization=1,keylen=32 \ --hash pbkdf2:digest=sha256,iterations=100000,keylen=32 \ --hash argon2:memory=65536,time=2,parallelism=1 \ --hash sha256 \ --hash md5 \ --hash hmac \ --hash ldap \ --mfa totp:rate=0.3 \ --mfa sms:rate=0.1 \ --mfa email:rate=0.1 \ --mfa webauthn:rate=0.05 \ --mfa recovery_codes:rate=0.2 \ --seed 42 \ --out ./fixtures/Hash algorithms are spread evenly across users. Each MFA type is drawn independently, so some users end up with several factors, such as totp plus recovery_codes. Together this covers:
| Case | Why it matters for Auth0 |
|---|---|
bcrypt cost 10 | The one case Auth0’s simple password_hash field accepts |
bcrypt cost 12 | Needs custom_password_hash |
scrypt, pbkdf2, argon2 | Modern KDFs, each with parameters that must survive translation |
sha256, md5 | Legacy unsalted/salted digests |
hmac, ldap ({SSHA}) | Keyed hashes and LDAP directory exports |
totp, sms, email | MFA enrollments Auth0 can import |
webauthn, recovery_codes | MFA Auth0 can’t import. Affected users need follow-up |
The command writes:
fixtures/├── users.cmf.jsonl.gz # the users, in CMF├── manifest.json # counts per hash algorithm and non-portable MFA type├── answer-key.json # cleartext password + TOTP secret per user (gitignored)└── .gitignoreanswer-key.json is what lets you prove imported users can actually sign in. Keep it out of version control.
3. Review the manifest
Section titled “3. Review the manifest”cat fixtures/manifest.json{ "record_count": 200, "hash_algorithm_counts": { "argon2": 22, "bcrypt": 46, "hmac": 22, "ldap": 22, "md5": 22, "pbkdf2": 22, "scrypt": 22, "sha256": 22 }, "non_portable_mfa_counts": { "recovery_codes": 48, "webauthn": 14 }}With a real export, this is where you find out which algorithms you’re dealing with. non_portable_mfa_counts tells you, before importing anything, how many users will need to re-enroll or regenerate recovery codes.
4. Validate against Auth0 (no network calls)
Section titled “4. Validate against Auth0 (no network calls)”iamigrate validate --in ./fixtures/users.cmf.jsonl.gz --target auth0checked 200 users against auth0: 0 problem(s)validate checks every user against what Auth0 can import:
- Password algorithms: all eleven
custom_password_hashalgorithms are supported:bcrypt,scrypt,pbkdf2,argon2,md4,md5,sha1,sha256,sha512,hmac,ldap. Any other algorithm is reported as a problem. - MFA: a portable factor Auth0 can’t import is reported. For example,
pushenrollments fail withunsupported MFA type "push".
Factors marked non-portable in CMF (webauthn, recovery_codes) are not validation errors. By definition they can’t move to any provider, so the import skips them and lists the affected users in the report (see Step 7).
5. Understand how hashes and factors are translated
Section titled “5. Understand how hashes and factors are translated”You don’t configure anything in this step. It explains what ends up in Auth0, which helps when you read the import report.
Password hashes
Section titled “Password hashes”| CMF algorithm | Auth0 field | Notes |
|---|---|---|
bcrypt ($2a$/$2b$, cost 10) | password_hash | Only without --upsert. password_hash can be set once and never updated. |
bcrypt (any other cost, or $2y$) | custom_password_hash (bcrypt) | |
scrypt | custom_password_hash (scrypt) | Requires params.keylen and a power-of-two params.cost |
pbkdf2 | custom_password_hash (pbkdf2) | Re-encoded as a PHC string. Requires a salt. |
argon2 | custom_password_hash (argon2) | Requires the full PHC string ($argon2id$v=19$...) |
md4, md5, sha1, sha256, sha512 | custom_password_hash | Hash (and salt) must be hex or base64 encoded. The salt position is preserved. |
hmac | custom_password_hash (hmac) | Requires hash.digest and the HMAC key |
ldap | custom_password_hash (ldap) | {CRYPT} values are not supported by Auth0 |
Any hash marked portable: false | none | The user is imported without a password and listed under requires_password_reset |
A user whose hash is missing a required parameter isn’t sent to Auth0. It is reported as TRANSLATION_ERROR in failed.
MFA factors
Section titled “MFA factors”| CMF factor | Auth0 result |
|---|---|
totp | Imported as {"totp": {"secret": ...}}. The user keeps their existing authenticator app. |
sms | Imported as a verified phone factor |
email | Imported as a verified email factor |
webauthn, push | Not imported. The user is listed under requires_reenrollment. |
recovery_codes | Not imported. The user is listed under requires_recovery_code_regen. |
Profile
Section titled “Profile”source_id becomes the Auth0 user_id, and Auth0 adds the connection prefix (so fx_ae0b6d305f0f4a20 becomes auth0|fx_ae0b6d305f0f4a20). The primary email and its verified flag, username, the first phone number and its verified flag (as phone_number and phone_verified), given_name, family_name, name, nickname, picture, blocked, app_metadata, and user_metadata are copied as-is.
Auth0’s bulk import requires an email. A user whose only identifiers are a username or a phone number isn’t sent to Auth0, and is reported under failed with TRANSLATION_ERROR.
6. Run the import
Section titled “6. Run the import”iamigrate import auth0 \ --in ./fixtures/users.cmf.jsonl.gz \ --connection-id con_XXXXXXXXXXXXXXXXimported: 173 succeeded, 27 failed -> report fixtures/import-report.json 27 failed: MFA_FACTORS_FAILEDUsers that already exist in the connection aren’t counted as failures. They’re listed on a separate line, for example 12 already exist: not updated; re-run with --upsert to update them.
The test data generator currently writes SMS phone numbers without the +<country code> prefix Auth0 requires, so expect the 27 users with an sms factor to fail with MFA_FACTORS_FAILED. This is a useful dry run of the format issue described in Step 4. To get a clean run, leave out --mfa sms:rate=0.1 when you generate the fixture.
Chunks are submitted one at a time and each job is polled until it finishes, so Auth0’s limit of two concurrent import jobs per tenant isn’t a concern. Don’t start a second import against the same tenant while one is running.
7. Handle users that need follow-up
Section titled “7. Handle users that need follow-up”Open the report:
cat fixtures/import-report.json{ "succeeded": ["fx_ae0b6d305f0f4a20", "..."], "failed": [ { "source_id": "fx_...", "code": "MFA_FACTORS_FAILED", "message": "..." } ], "requires_reenrollment": ["fx_a91990ed808aa2cc", "..."], "requires_recovery_code_regen": ["fx_e9e838f2b3b9f8fe", "..."]}| Field | What to do |
|---|---|
failed | Each entry has a source_id, an Auth0 error code, and a message. See Troubleshooting. |
requires_password_reset | The user has no importable password. Send a password reset email, or let them use “Forgot password” at first login. |
requires_reenrollment | WebAuthn/passkey or push enrollments were dropped. Ask the user to enroll again at next login. |
requires_recovery_code_regen | The old recovery codes don’t work in Auth0. The user gets a new one when they next sign in with MFA. |
To get a list you can feed into an email campaign or a script:
jq -r '.requires_reenrollment[]?' fixtures/import-report.json > reenroll.txt8. Verify the import
Section titled “8. Verify the import”Reconcile with the tenant
Section titled “Reconcile with the tenant”iamigrate diff auth0 --in ./fixtures/users.cmf.jsonl.gzThis looks up every user by email, or by username or phone number for users without one, and reports any that are missing from the tenant, plus any mismatch in blocked status. Users with none of the three are listed under no identifier.
Prove that passwords and TOTP work
Section titled “Prove that passwords and TOTP work”A user showing up in the tenant doesn’t prove their hash was translated correctly. Pick at least one user per algorithm from answer-key.json, then sign in as each one:
jq '.entries[0]' fixtures/answer-key.json{ "source_id": "fx_ae0b6d305f0f4a20", "email": "jonathon.marquardt@wilkinson.biz", "password": "A@MkjAZV74rJ8XWZ"}Use Authentication → Database → your connection → Try Connection in the Dashboard, or any application that has the connection enabled. For users with an imported TOTP factor, the answer key also includes their totp_secret. Generate the current code with any TOTP tool, for example:
oathtool --totp -b <totp_secret>A successful sign-in shows that the hash parameters, salt, and encoding were all translated correctly.
Clean up the rehearsal
Section titled “Clean up the rehearsal”Delete the test users before you import real ones. Remove them from User Management → Users, or delete the connection and recreate it. Users deleted by other means can still exist in Auth0’s user store, which causes DUPLICATED_USER on later imports.
9. Run the real migration
Section titled “9. Run the real migration”Once the rehearsal passes, repeat steps 3–8 with your real data:
- Export your source into CMF with
iamigrate export, for example from a flat CSV/JSON file or from Ory Kratos (see Export users from Ory Kratos). - Review
mapping.yamlwithiamigrate map, especially custom fields headed forapp_metadata/user_metadata. - Run
validateand fix every reported problem at the source. - Import a small canary batch first, and verify logins for it.
- Import the rest, then run
diff auth0and handle the follow-up lists.
Troubleshooting
Section titled “Troubleshooting”| Error code / symptom | Cause | Fix |
|---|---|---|
TRANSLATION_ERROR | The CMF hash is missing a parameter Auth0 needs (for example scrypt keylen, a pbkdf2 salt, or an argon2 PHC string) | Fix the export for that algorithm and re-import the affected users |
DUPLICATED_USER | Without --upsert: the user already exists in the connection. With --upsert: the user is left over in Auth0’s user store (possibly from an earlier, deleted import) | Without --upsert: re-run with --upsert to update existing users. With --upsert: delete the user through the Connection Users endpoint, then re-import. |
MFA_FACTORS_FAILED | An MFA value doesn’t match Auth0’s format: a phone number without + and country code, or a TOTP secret that isn’t unpadded Base32 | Normalize the values in the export |
| Login fails for one algorithm only | Wrong hash parameters or encoding for that algorithm | Check the CMF params/encoding against the source system. Re-import with --upsert (only possible if the user hasn’t logged in yet). |
401/403 from the Management API | Expired token, wrong client credentials, or missing scope | Get a new token (or check AUTH0_CLIENT_ID/AUTH0_CLIENT_SECRET), and check the scopes in Step 1 |
Import job failed | Auth0 rejected the job itself (for example, a job timed out after 2 hours) | Re-run. Users that already succeeded report as duplicates unless you pass --upsert. |