Skip to Content
🔐 Faable AuthMigration GuidesImport & export password hashes

Import and export password hashes

Short answer: Faable Auth imports the password hashes your current provider exports. That covers bcrypt, scrypt (including Firebase’s modified scrypt), PBKDF2 and Argon2id. Your users sign in with the password they already have, with no reset email and no forced password change. On each user’s first successful sign-in, Faable re-hashes the password with Argon2id. Import and export are available on every plan, including Free.

faable auth users import users.ndjson --from auth0 --dry-run # check first faable auth users import users.ndjson --from auth0 # then import

What can be imported

SourceWhat its export containsAlgorithm
Auth0NDJSON from a support ticket, with passwordHashbcrypt
ClerkCSV from the dashboard, password_digest + password_hasherbcrypt, Argon2, PBKDF2-SHA256, Firebase scrypt
Firebasefirebase auth:export --format=json, passwordHash + saltFirebase modified scrypt
SupabaseA query on auth.users, encrypted_passwordbcrypt
KeycloakRealm export (kc.sh export), credentials[]PBKDF2-SHA256/SHA512, Argon2
Faablefaable auth users export --include-hasheswhatever the source tenant holds

Faable refuses two kinds of hash, and reports each refused user separately without failing the rest of the batch:

  • Weak hashes (weak_password_hash): fast digests such as MD5, SHA-1 or plain SHA-256, PBKDF2 with SHA-1, and costs below a floor. The floors are bcrypt cost 10, PBKDF2 10,000 iterations, scrypt N = 2¹⁴, and Argon2 at the level of the OWASP table  (7 MiB × 5 passes up to 46 MiB × 1 pass).
  • Hashes too expensive to verify on every sign-in (invalid_password_hash): bcrypt above cost 13, or scrypt or Argon2 needing more than 64 MiB per sign-in. Unreadable values get the same error.

Those users sign in with an emailed code (passwordless, Hobby and up) or set a new password through Forgot password.

Import with the CLI

  1. Get the export from your current provider (see each source below).

  2. Log in, with faable login, as someone who manages the tenant.

  3. Dry run. The server checks every row and reports what it would do, and nothing is written:

    faable auth users import export.ndjson --from auth0 --dry-run \ --auth-url https://<account>.auth.faable.link
  4. Import. Records are sent in batches of 500. Users that already exist (matched by email) are skipped, so you can run the same file again after fixing errors:

    faable auth users import export.ndjson --from auth0 \ --map-connection google-oauth2=google

The CLI prints one line per refused record and a summary. --json gives a report you can script against, and the exit code is non-zero if any record failed.

Useful flags:

  • --connection <name>: the database connection the passwords go to, when the tenant has more than one.
  • --map-connection provider=connection: rename a social provider to your Faable connection name, so identities link to the right connection.
  • --update-existing: set the password hash of users who already exist, instead of skipping them. Their profile is not overwritten.

What happens to each user

  • The user is created with their profile and email_verified. A user verified at the old provider arrives verified, with email_verified_method: import.
  • Social identities (Google, GitHub…) in the export are linked to the user, so their first social sign-in finds the imported account instead of creating a new one.
  • Imported users get no emails: no verification link, no welcome and no “user created” notification. A migration of 10,000 users sends nothing. user.created webhooks still fire, carrying imported_at so you can filter them.
  • The hash is never returned by the API and never written to logs. The audit log records only the algorithm.
  • On the first successful sign-in, the password is re-hashed with Argon2id (OWASP parameters), and the imported hash is gone.

Per-source notes

Auth0

Auth0 exports password hashes only through a support ticket, and not on the Free tier. You receive NDJSON with a passwordHash field in bcrypt. Social users appear as identities; their Auth0 connection names (google-oauth2) usually need --map-connection.

Clerk

Export the CSV from the Clerk dashboard. The password_hasher column says which algorithm each row uses. Rows hashed with MD5 or SHA-256 are refused as weak.

Firebase

firebase auth:export users.json --format=json exports the hashes, but not the project-wide parameters needed to verify them. Copy those from the Firebase console (Authentication → Users → ⋮ → Password hash parameters) and pass them to the CLI:

faable auth users import users.json --from firebase \ --hash-key <base64_signer_key> --salt-separator <base64_salt_separator> \ --rounds 8 --mem-cost 14

Phone-only and anonymous users have no email, so they are reported and skipped.

Supabase

You own auth.users, so the export is a query. Save it as CSV (or JSON with --format json):

select email, encrypted_password, email_confirmed_at, raw_user_meta_data from auth.users;

Keycloak

Use a realm export (kc.sh export --users realm_file) or one of its <realm>-users-N.json files. Both PBKDF2-SHA256/SHA512 and the Argon2 default of Keycloak 24+ import. Plain pbkdf2 (SHA-1) is refused as weak. Identity-provider links (federatedIdentities) become identities.

Import over the Management API

The CLI is a client of two endpoints, which you can call directly from your own migration code.

POST /user/import takes up to 500 users per request and answers one status per row: created, updated, skipped or error. It needs the create:users and update:credentials permissions.

{ "connection": "Username-Password", "dry_run": false, "users": [ { "email": "ana@example.com", "email_verified": true, "name": "Ana", "password_hash": "$2b$10$…", "identities": [{ "connection": "google", "identity_id": "10987654321" }] }, { "email": "kc@example.com", "password_hash": { "algorithm": "pbkdf2-sha256", "hash": "<base64>", "salt": "<base64>", "params": { "iterations": 27500 } } } ] }

password_hash is either a self-describing string or its parts:

  • Strings: bcrypt $2a$/$2b$/$2y$, Argon2 PHC $argon2id$…, scrypt PHC $scrypt$ln=…, PBKDF2 $pbkdf2-sha256$i=…, or Django’s pbkdf2_sha256$….
  • Parts: { algorithm, hash, salt, params } for scrypt, PBKDF2 and firebase-scrypt. Firebase’s params are signer_key, salt_separator, rounds and mem_cost.

For a single user, POST /credentials/{id}/set-password accepts password_hash in place of password.

Export

Leaving Faable, or moving users between two of your tenants, works the same way in reverse:

faable auth users export users.ndjson --include-hashes faable auth users import users.ndjson --from faable --auth-url https://<other>.auth.faable.link

The export is NDJSON in the same shape the import takes. Linked identities are included, but their provider access and refresh tokens are not.

Exporting hashes is the most sensitive thing the Management API does, so it is protected on several levels:

  • It needs its own permission, read:password_hashes; read:users alone exports users without hashes.
  • The CLI writes the file readable only by you (0600), and refuses to print hashes to a terminal unless you pass --force.
  • Every page is written to the audit log as credentials.exported, and the first page fires a credentials.exported event. Subscribe to it by email or webhook so an export never happens unnoticed.

Passwords stored before August 2026 that have not been used since are in a legacy format that is not exported (password_hash_status: legacy_unexportable). They upgrade the next time the user signs in.

Last updated on