Product documentation
Migrate from Auth0 or Amazon Cognito
A step-by-step runbook for moving a user base off Auth0 or Amazon Cognito onto Thoryn using the turnkey vendor-export import endpoints — the export commands, the field mapping, an honest account of which credentials carry over, and cutover and rollback.
Migrate from Auth0 or Amazon Cognito
This is a runbook for moving an existing user base off Auth0 or Amazon Cognito onto Thoryn. Both vendors export in their own JSON shape, so Thoryn ships a turnkey import endpoint per vendor: you upload the vendor's own export file unchanged, and Thoryn maps it into a user directory for you. No CSV wrangling, no reshaping into a platform format.
The one decision that shapes the whole migration is the same for every provider: will your source hand you the password hashes? It decides whether your users keep their passwords or all pass through a reset.
- Auth0 stores bcrypt hashes and can release them — but not through the self-service export. Getting them out is a separate, manual step (see below). When you have them, users keep their passwords and never see a reset.
- Amazon Cognito never exports password hashes — there is no API that returns them. Every migrated Cognito user is created password-less and re-establishes a credential through password reset or federation. This is inherent to Cognito, not a Thoryn limitation.
This runbook covers the vendor-specific endpoints only. For the full migration picture — the three strategies (bulk import, trickle migration that verifies at first login, and federate-and-coexist), the deep password-hash parameter rules, and what carries across versus what does not — read Migrate your users to Thoryn first. Everything here builds on the bulk-import strategy described there.
Auth0 vs Cognito at a glance
| Auth0 | Amazon Cognito | |
|---|---|---|
| Turnkey endpoint | POST /api/v1/users/import/auth0 | POST /api/v1/users/import/cognito |
| Export shape | Flat JSON array of user objects | JSON array of Username + Name/Value Attributes |
| Password hashes in the export | bcrypt — but only via an Auth0 support request | None, ever |
| User experience after migration | Keeps their password, no reset | Password reset or federation on first sign-in |
| Federated users | Carried for correlation (identities[]) | Carried for correlation (identities / EXTERNAL_PROVIDER) |
Both endpoints share the same import pipeline, row caps, partial-failure contract,
and job model as the native /api/v1/users/import surface — the vendor endpoint only
adds the mapping step in front. You poll, list, and cancel jobs through the one shared
/api/v1/users/import/jobs surface; there is no parallel Auth0 or Cognito job system.
Before you start
You need:
- A tenant admin token carrying the
tenant:users.importscope. This is a scope of its own, deliberately separate fromtenant:users.write, because bulk-writing a whole directory (with credentials that grant sign-in) is a far larger blast radius than single-user CRUD. Grant it to a one-off migration client and revoke it when you are done. It is granted on the customer-plane clients by hub migrationV91; if your token does not carry it, ask your Thoryn contact to enable it. - A target tenant your production traffic is not pointed at yet. Import there, verify there, then cut traffic over. The import only ever creates — an existing email is skipped, never overwritten — so this is the safe sequencing, not a mandatory one.
- A way to drive the import. Today the import is driven through the API/CLI — the
vendor endpoints below, run with
curlandjq(or a language client) — which is what the rest of this runbook shows, and the supported path for CI, scripted cut-overs, and runs you keep under version control. Use the server-side dry-run (Step 3 below) as your validate-before-commit step: it returns the per-row prediction — what would be created, skipped, or fail — before a single row is written. A guided console migration wizard (upload the vendor export → Validate → Commit, with that same dry-run wired in as its Validate step) is planned but not yet available; it is built in the console (a separate repository), and until it ships the API/CLI path above is how you migrate.
Throughout, $TOKEN is your tenant admin bearer token and api.thoryn.org is the
public API host; on a self-managed deployment substitute your own gateway host.
Part 1 — Migrate from Auth0
Step 1: Export your Auth0 users
Auth0 splits what you need across two exports, and this is the part most Auth0 migrations underestimate.
Profiles (self-service). Get the user records — emails, names, verification flags,
user_id, linked identities — with either:
-
The User Import / Export dashboard extension (Extensions → "User Import / Export" → Export), which produces a gzipped JSON or CSV; or
-
The Management API bulk-export job:
curl -X POST https://YOUR_TENANT.auth0.com/api/v2/jobs/users-exports \ -H "Authorization: Bearer $AUTH0_MGMT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "connection_id": "con_xxxxxxxx", "format": "json", "fields": [ { "name": "email" }, { "name": "email_verified" }, { "name": "given_name" }, { "name": "family_name" }, { "name": "name" }, { "name": "nickname" }, { "name": "picture" }, { "name": "user_id" }, { "name": "identities" }, { "name": "blocked" } ] }'The job returns an id; poll
GET /api/v2/jobs/{job_id}for the signed download URL.Both routes give you a JSON array of user objects — but neither includes the password hash. Auth0 does not expose stored hashes through the Management API or the extension, by design.
Password hashes (support request — the manual step). To move passwords without a
reset you need the bcrypt custom_password_hash records, and the only way to obtain them
is to open a support request with Auth0 asking for a bulk password-hash export for the
connection you are migrating. Auth0 delivers the hashes out of band. This is a
prerequisite you cannot self-serve, so plan for its lead time.
If you do not get the hashes, an Auth0 migration is a profiles-only import: every user is created password-less and must reset, or you run trickle migration so passwords verify at first login without ever moving the hash. Decide this before you import — it changes the user experience for everyone.
Assemble the import file. Thoryn's /auth0 endpoint accepts the standard Auth0
export shape directly: a JSON array of user objects. Where you have the hashes, add each
user's custom_password_hash object (or a raw passwordHash string) to their record.
Unknown Auth0 fields are ignored, so you can upload the export largely as-is:
[
{
"email": "ada@example.com",
"email_verified": true,
"given_name": "Ada",
"family_name": "Lovelace",
"user_id": "auth0|5f7c8ec7c33c6c004bbafe82",
"custom_password_hash": {
"algorithm": "bcrypt",
"hash": "$2b$10$N9qo8uLOickgx2ZMRZoMy..."
}
},
{
"email": "grace@example.com",
"name": "Grace Hopper",
"identities": [ { "provider": "google-oauth2", "connection": "google", "isSocial": true } ]
}
]Step 2: Know what carries across
The mapper reads only the subset of the Auth0 record the platform's user directory can
hold. Everything else (app_metadata, user_metadata, logins_count, last_login,
multifactor, …) is ignored, not an error.
| Auth0 field | Thoryn user field | Notes |
|---|---|---|
email | email | Required — the tenant-unique key. A record with no email is an ERROR row. |
email_verified | emailVerified | Defaults to false when absent. |
given_name / family_name | given / family name | Used directly when either is present. |
name → else nickname → else email local-part | given / family name | When given_name / family_name are absent, a single name is split on the last space; a lone nickname or the email local-part is the fallback. |
user_id | externalId | Retained for correlation back to the Auth0 record. |
picture | picture | Straight copy. |
blocked: true | suspended (enabled = false) | The user is created but cannot authenticate until an admin clears it. |
custom_password_hash / passwordHash | password (pre-hashed) | See credential carry-over. |
primary non-auth0 identities[].provider / connection | federationProvider | For a password-less federated user, the provider name is retained for correlation (see below). |
Federated (social / enterprise) users. A user whose primary Auth0 identity is not
auth0 — a Google, SAML, or OIDC connection — has no local password to bring, so the
provider name is carried on federationProvider for correlation. For a social login that
is the provider (google-oauth2, …); for an enterprise connection whose provider is a
generic protocol marker (samlp, oidc, waad, …) the specific connection name is
recorded instead, because that is what a Thoryn federation member corresponds to.
On both the synchronous /auth0 endpoint (and its ?dryRun=true pre-flight) and the
asynchronous /auth0/jobs submission, that name is resolved against your tenant's configured
federation members: a match canonicalises the row to the member's provider type, and a
provider that matches no configured member is flagged (unresolved_federation_provider) instead
of imported with a dangling reference. On the sync path a rejection is one of the per-row
results; on the async path the enqueued job only ever receives resolved rows and the rejections
ride back on the 202 submission response's rejected array — so a job never carries an
unresolvable provider. Attach the corresponding federation member
before importing its users (and dry-run first to surface any unresolved providers). A tenant
with no federation members configured yet still carries the name verbatim, nothing to resolve
against — see Known limitations.
Step 3: Dry-run the whole export first
Do not import 40,000 users blind. Add ?dryRun=true to the same endpoint to validate
the entire export without writing a single row. Every row runs the exact same
validation a real import runs — hash usability, missing email, duplicate email,
unmappable fields — and you get the per-row outcome back, but nothing is created:
curl -X POST 'https://api.thoryn.org/api/v1/users/import/auth0?dryRun=true' \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @auth0-export.jsonThe response is the ordinary import shape, read as a prediction: CREATED is a row
that would be created, SKIPPED_DUPLICATE one that would be skipped, and ERROR one
that would fail with a stable reason code. Fix the format and hash problems it surfaces
while it is still free — before any write:
{
"imported": 39997,
"skipped": 1,
"failed": 2,
"results": [
{ "email": "ada@example.com", "status": "CREATED" },
{ "email": "bad@example.com", "status": "ERROR", "reason": "malformed_password_hash" }
]
}The flag works identically on the native POST /api/v1/users/import and the
POST /api/v1/users/import/cognito endpoints, and it is inherently synchronous — there
is no dry-run job. A dry-run is capped and gated exactly like a real import (empty_batch
/ batch_too_large and the tenant:users.import scope all still apply), so what passes a
dry-run is what a commit will accept.
A dry-run writes nothing, so it proves the file maps and validates — but it cannot prove a user can log in (there is no password to test against a hash that is not stored yet). Once the dry-run is clean, do a real one-row import of a user whose password you know and sign in as them to exercise the whole login chain:
curl -X POST https://api.thoryn.org/api/v1/users/import/auth0 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[ { "email": "you@example.com", "custom_password_hash": { "algorithm": "bcrypt", "hash": "$2b$10$..." } } ]'A CREATED row means the hash passed Thoryn's usability check; a successful login proves
the rest of the chain. Do both before the bulk run — together they catch the two failure
modes (a malformed export, and a hash that stores but will not verify) while it is still
cheap to fix.
Step 4: Import the full export
Up to 500 users — synchronous. Post the whole export array and get the per-row outcome back immediately:
curl -X POST https://api.thoryn.org/api/v1/users/import/auth0 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @auth0-export.jsonMore than 500 users — asynchronous job. Same body, the /jobs variant, up to 10,000
users:
curl -X POST https://api.thoryn.org/api/v1/users/import/auth0/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @auth0-export.jsonYou get 202 Accepted with a Location header and the job body. Poll it on the shared
jobs surface — note the path has no auth0 segment, because it is the one shared job
model:
curl https://api.thoryn.org/api/v1/users/import/jobs/JOB_ID \
-H "Authorization: Bearer $TOKEN" | jq{
"id": "9f1c...",
"status": "in_progress",
"rowsTotal": 8400,
"rowsProcessed": 3200,
"imported": 3105,
"skipped": 92,
"failed": 3,
"errors": [ { "rowIndex": 118, "email": "bad@example.com", "reason": "malformed_password_hash" } ],
"errorsTruncated": false
}Listing and cancelling jobs work exactly as described in
Migrate your users — the same
GET /api/v1/users/import/jobs and POST /api/v1/users/import/jobs/{id}/cancel.
Partial success is the contract. One bad row never aborts the batch — it becomes an
ERROR result with a stable reason code and processing continues. A user whose email
already exists is SKIPPED_DUPLICATE, never overwritten, so re-running the same export
is safe (idempotent by skip). Only an empty array (empty_batch) or an over-cap batch
(batch_too_large) rejects the whole request.
Step 5: Finish the profile
After the users land, wire up the parts that no import path carries — see what does not migrate. In short: MFA enrolments, metadata, roles, and organization membership all carry nothing and are set up after import. Plan the MFA re-enrolment prompt — it is the one thing your users will notice.
Auth0 credential carry-over
This is the part that decides whether the migration is invisible, so be precise about it.
bcrypt carries verbatim. Auth0 database connections store bcrypt, and Thoryn stores
the hash exactly as given and verifies it at the migrated user's first login. $2a, $2b,
and $2y all verify; the derived cost lives in the hash string, so any cost factor works.
The one exception is the $2x$ variant emitted by very old PHP crypt_blowfish sources,
which is genuinely unverifiable and rejected as malformed_password_hash.
Two source shapes are accepted, both from the Auth0 export:
custom_password_hash: { "algorithm": "bcrypt", "hash": "…" }, wherehashis either a bare string or a{ "value": "…", "encoding": "…" }object (the Auth0 bulk-import schema); orpasswordHash: "$2b$…"— a raw bcrypt string carried directly on a DB-connection export.
The mapper defaults the algorithm to bcrypt when the export omits it.
The turnkey Auth0 endpoint carries bcrypt,
argon2,scrypt, andpbkdf2. Each of these carries its parameters inside the hash string, so it verifies with no separate declaration — includingpbkdf2in the passlib / PHC modular-crypt form ($pbkdf2-sha256$<rounds>$<salt>$<checksum>) that Auth0 exports for a custompbkdf2connection. The one shape the turnkey path still cannot carry is a bare-hexpbkdf2(salt‖hashwith no embedded parameters), which needs the KDF-parameter declaration only the native import carries; that is not how Auth0 exportspbkdf2, so an Auth0 export is unaffected. See Known limitations and the password-hash parameter rules.
A hash that would fail at login is rejected at import, as a per-row ERROR, while you
can still do something about it — never stored to fail silently weeks later. The row
reason codes are shared with the native import.
Part 2 — Migrate from Amazon Cognito
Step 1: Export your Cognito user pool
Cognito has no bulk-export job; you page through the pool with the AWS CLI:
aws cognito-idp list-users --user-pool-id us-east-1_XXXXXXXXX --max-results 60 \
--output json > cognito-page-1.jsonlist-users returns at most 60 users per call with a PaginationToken; loop, passing
--pagination-token, until it is absent, then concatenate the Users arrays into one
JSON array. Each element looks like:
{
"Username": "ada@example.com",
"Attributes": [
{ "Name": "sub", "Value": "a1b2c3d4-..." },
{ "Name": "email", "Value": "ada@example.com" },
{ "Name": "email_verified", "Value": "true" },
{ "Name": "given_name", "Value": "Ada" },
{ "Name": "family_name", "Value": "Lovelace" }
],
"Enabled": true,
"UserStatus": "CONFIRMED"
}Thoryn's /cognito endpoint accepts this array directly — both the list-users
Attributes name and the admin-get-user UserAttributes name are understood.
There is no password step. Cognito exposes no API that returns a password hash, so there is nothing to export and nothing to add to the file. Every migrated user is password-less by necessity. Skip straight to the import.
Step 2: Know what carries across
The mapper flattens the Attributes list and reads the standard OIDC claims off it.
Phone numbers, MFA state, and any custom:* attributes are read but not mapped —
dropped, never made to fail a row. The account Enabled flag is carried (see below).
| Cognito attribute / field | Thoryn user field | Notes |
|---|---|---|
email | email | Required — the tenant-unique key. |
email_verified (string "true") | emailVerified | Cognito serialises booleans as strings; anything but "true" is false. |
given_name / family_name | given / family name | Used directly when present. |
name (when given/family absent) | given / family name | Best-effort split on the last space. |
sub → else Username | externalId | sub is Cognito's immutable id; Username may be an email, a UUID, or a Provider_id, so sub is preferred for correlation. |
locale | locale | Straight copy. |
picture | picture | Straight copy. |
identities[].providerName / EXTERNAL_PROVIDER status | federationProvider | Retained for correlation; see below. |
Enabled: false | suspended (enabled = false) | The user is created but cannot authenticate until an admin clears it — symmetric with an Auth0 blocked user. |
A disabled Cognito user (
Enabled: false) imports suspended (enabled = false), symmetric with an Auth0blockeduser — the account is created but cannot authenticate until an admin re-enables it through the users API. A missing ortrueEnabledimports the user active.
Federated users. A user whose UserStatus is EXTERNAL_PROVIDER, or who carries an
identities attribute, is federated. federationProvider is populated from the
identities attribute's providerName when present, else from the Username prefix
convention (Google_…, Facebook_…, <provider>_…). As with Auth0, both the synchronous
/cognito endpoint (and its ?dryRun=true pre-flight) and the asynchronous /cognito/jobs
submission resolve that name against your tenant's configured federation members —
canonicalising a match and flagging an unresolvable provider (unresolved_federation_provider),
per-row on the sync path or in the 202 submission's rejected array on the async path — so
attach the corresponding federation member before importing its
users. A tenant with no members configured yet carries the name verbatim (see
Known limitations).
Step 3: Import the export
Identical to the Auth0 endpoints, on the /cognito path — synchronous up to 500,
asynchronous up to 10,000:
# Synchronous — up to 500 users
curl -X POST https://api.thoryn.org/api/v1/users/import/cognito \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @cognito-export.json
# Asynchronous job — up to 10,000 users
curl -X POST https://api.thoryn.org/api/v1/users/import/cognito/jobs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @cognito-export.jsonPoll, list, and cancel on the shared /api/v1/users/import/jobs surface, exactly as for
Auth0. Partial success and idempotent-by-skip re-runs work the same way.
Step 4: Get every user a credential
Because no Cognito user has a password after import, you must give each one a way in:
- Federated users (the
federationProviderwas set) sign in through their IdP once you attach the matching federation member. No reset needed. - Password users re-establish a credential through the standard forgot-password flow: the imported account exists (password-less), so a reset request for that email issues a reset link. Plan a communication campaign telling users to reset on first visit, and consider tailoring the reset email.
There is no bulk "send reset to everyone" endpoint — reset is user-initiated by design, so a mass campaign is a mail-out that points users at the forgot-password page, not an API call. If Cognito passwords matter enough to preserve, migrate off Cognito via federate-and-coexist instead and keep Cognito as the IdP until you are ready to drop the credential.
Endpoint reference
All four vendor endpoints are on product-api, reached through the api-gateway, and gated by
tenant:users.import. They import into the tenant your token's tnt claim names — you
cannot address another tenant, and another tenant's job id is a 404, never a 403.
| Method | Path | Description |
|---|---|---|
POST | /api/v1/users/import/auth0 | Import an Auth0 export synchronously (≤ 500 users). Per-row outcome. |
POST | /api/v1/users/import/auth0/jobs | Submit an async Auth0-export job (≤ 10,000 users). Returns 202 + job. |
POST | /api/v1/users/import/cognito | Import a Cognito export synchronously (≤ 500 users). Per-row outcome. |
POST | /api/v1/users/import/cognito/jobs | Submit an async Cognito-export job (≤ 10,000 users). Returns 202 + job. |
GET | /api/v1/users/import/jobs | List the tenant's import jobs (shared with native/Auth0/Cognito). |
GET | /api/v1/users/import/jobs/{id} | Poll a job's status, counters, and per-row errors. |
POST | /api/v1/users/import/jobs/{id}/cancel | Cancel a pending / in_progress job. |
Full request/response schemas are in the generated
user-import API reference. The native
POST /api/v1/users/import (platform-shaped body) and the whole job model are documented in
Migrate your users.
Troubleshooting
malformed_request_body (400) on the whole request. The uploaded file is not valid
JSON, or its root is not the array the endpoint expects (an Auth0 export or a Cognito export
is a JSON array of user objects). The body carries credentials, so the parser's message
is never echoed back — validate the file with jq . file.json locally.
malformed_password_hash on Auth0 rows. Most often a $2x$ hash from an old PHP
crypt_blowfish source — genuinely unverifiable here, not a naming quirk. Re-hash at
source into a standard bcrypt variant, or move those users via trickle migration.
missing_password_hash_parameters on Auth0 rows. A pbkdf2 hash arrived as a bare
salt‖hash with no embedded parameters — the one pbkdf2 shape the turnkey path cannot
carry, because it needs the KDF-parameter declaration only the native import supplies. Auth0
exports a custom pbkdf2 connection in the self-describing passlib form
($pbkdf2-sha256$…), which does import and verify, so this is unusual for an Auth0
export; re-hash those users into bcrypt at the source, or move them with trickle migration.
Everything comes back SKIPPED_DUPLICATE. The users already exist in that tenant —
import never overwrites. You are probably re-importing into a tenant that was imported
already, which is harmless. To change existing users, use the
users API, not import.
Cognito rows import but emailVerified is false everywhere. Cognito serialises
email_verified as the string "true"/"false". The mapper handles that — but if your
export tooling dropped or renamed the attribute, verification will not carry and your users
re-verify on first login. Confirm the attribute is present and named email_verified.
Cognito users cannot sign in after import. Expected — they are password-less by design. Send them through password reset or attach their federation member. This is not a failure.
403 on every call. Your token lacks tenant:users.import — a scope of its own,
separate from tenant:users.write.
batch_too_large under 10,000 rows. You posted to a synchronous endpoint (/auth0 or
/cognito), which caps at 500. Use the /jobs variant.
413 payload_too_large from the gateway. The request body exceeds the gateway cap for
the import path. Split the export across jobs — the import is idempotent-by-skip, so
overlapping batches are safe. (See the gateway body-cap note in
Migrate your users.)
For the shared job-lifecycle errors (job_not_found, job_not_cancellable,
invalid_cursor, upstream_identity_error, facade_not_configured) and every row
reason code, see the native import guide — the
vendor endpoints share all of them.
Rollback
The safe-by-construction posture is the same as any Thoryn import:
- Nothing is overwritten. Import only ever creates; an existing user is skipped untouched. A mistaken re-run cannot corrupt correct data.
- Your source is untouched. These endpoints read an export file. Nothing writes back to Auth0 or Cognito — your rollback is to keep your apps pointed at the old provider until you are ready.
- Clean up per user through the users API if a batch went to the wrong tenant. There is no "undo import" endpoint and you do not need one: import into a tenant production traffic is not pointed at yet, verify, then cut over.
Known limitations
Stated plainly so you can plan around them:
- Very large directories are split across async jobs. Large runs commit through the
async
/jobsendpoints, which cap at 10,000 users per submission. A multi-hundred-thousand-user directory is therefore split across several jobs; the import is idempotent-by-skip, so overlapping or re-run batches are safe. (The planned console migration wizard, when it ships, will drive these same/jobsendpoints — see the note on the wizard under Before you start.) - Auth0 password hashes are not self-service. They require an Auth0 support request; the Management API and the Import/Export extension export profiles only.
- Only a bare-hex
pbkdf2hash cannot be carried.bcrypt,argon2,scrypt, andpbkdf2in the passlib modular-crypt form Auth0 exports ($pbkdf2-sha256$…) all carry and verify, because each embeds its own parameters. The lone exception is a bare-hexpbkdf2(salt‖hash, no embedded parameters): it needs the KDF-parameter declaration only the native import carries and is rejectedmissing_password_hash_parameterson the turnkey path. Auth0 does not exportpbkdf2in that shape, so an Auth0 export is unaffected; otherwise re-hash intobcryptat the source or use trickle migration. - Cognito exports no passwords. Every Cognito user resets or federates on first sign-in. This is a Cognito property, not a Thoryn one.
- Federation-member resolution runs on every vendor import path. A federated Auth0 /
Cognito user's provider name (
google-oauth2, a connection name, …) is resolved against your tenant's configured federation members on the synchronous/auth0and/cognitoendpoints (and their?dryRun=truepre-flight) and on the asynchronous/auth0/jobsand/cognito/jobssubmissions: a match canonicalises the row to the member's provider type, and a provider that matches no configured member is flagged (unresolved_federation_provider) rather than imported with a dangling reference. The sync path reports a rejection as a per-row result; the async path resolves before enqueue, so the job only ever receives resolved rows and the rejections come back on the202submission'srejectedarray (when every row is rejected there is nothing to enqueue — the response is200with a null job). Configure the federation member before importing its users, and run a dry-run to surface unresolved providers first. One case still carries the provider verbatim: a tenant with no federation members configured yet (nothing to resolve against). - No MFA, metadata, roles, groups, or organization membership carry through any import path — see what does not migrate.
Security notes
- Import payloads carry credentials and are never logged. The vendor endpoints, the mappers, and the upstream client all avoid logging request bodies; an async job's payload is encrypted at rest and wiped once the job reaches a terminal state.
- A hash that cannot verify is rejected, not stored. Storing a credential that can never authenticate anyone is a silent lockout, so the row fails loudly at import instead.
tenant:users.importis a high-blast-radius grant — it can write your whole directory, including sign-in credentials. Scope it to the migration client and revoke it when the migration is done.- Tenant isolation holds. You can only import into your own tenant (
tntclaim), and a job belonging to another tenant is indistinguishable from one that does not exist — both are404.
Related
- Migrate your users to Thoryn — the full strategy guide this runbook builds on: bulk import, trickle migration, federate-and-coexist, and the deep password-hash rules.
- Add a federation member — for users whose credential you keep
at the source, and the destination for the retained
federationProvidercorrelation. - user-import API reference — generated request/response schemas for every endpoint above.
- Customize notification emails — tailor the reset email Cognito users receive on first sign-in.
- Authorize with roles, permissions, and relationships — set up the roles that do not migrate.