SV Premium

Migration guides

Migrate authentication from Auth0 to self-hosted Keycloak

Move users and passwords, set up production Keycloak, and update your apps to use it. Includes staged cutover and the limits of rolling back to Auth0.

From Auth0 to Keycloak (self-hosted) · Published 2026-10-08

Before you commit

Hands-on effort
2–4 weeks of hands-on work
Elapsed time
Allow time for the hash export, staged cutover and a 1–3 month lazy-migration window if used.
Examples cover
Keycloak 26.8.0 · PostgreSQL · OIDC
Content review
2026-10-09
Integration testing
Not recorded. Rehearse the commands and rollback on staging.

Prerequisites

  • Auth0 tenant administration, application owners, staging users and a secure process for password exports.
  • Production Keycloak operations, managed PostgreSQL, backups, monitoring and an upgrade owner.

Don’t migrate yet if…

  • You cannot operate and patch an identity service or fund the ongoing work.
  • Required Auth0 Actions, MFA factors or enterprise connections have no tested replacement.

Auth0 sells self-serve plans in fixed MAU brackets. B2C Essentials costs $700 a month at 10,000 MAU and $2,100 at 30,000; above that you need an Enterprise contract. B2B Essentials costs $2,100 a month at 10,000 MAU, with 3 enterprise connections included and $100 a month for each extra one.

Keycloak has no user or connection license fees. You pay for infrastructure and the work of running it. A small setup can start with one 2-vCPU node and managed Postgres; production redundancy needs more. Compare your usage in the calculator.

The hard parts are moving passwords, changing token validation in every app, and running the service yourself. Name the person who will own upgrades and login incidents before you start.

0. Before you start

1. Run Keycloak in production

The examples use Keycloak 26.8.0 (released 2026-10-01). The project ships about four minor releases a year and puts security fixes only into the current minor, so plan to upgrade every quarter. The official image is quay.io/keycloak/keycloak.

Put the options in conf/keycloak.conf. db, health-enabled and metrics-enabled are build-time options, so run build once after setting them. Pass the database password as KC_DB_PASSWORD from your secret store rather than leaving it in the file:

db=postgres
db-url-host=keycloak-db.internal
db-username=keycloak
health-enabled=true
metrics-enabled=true
bin/kc.sh build
bin/kc.sh start --optimized --hostname https://auth.example.com \
  --https-certificate-file=/etc/keycloak/tls.crt \
  --https-certificate-key-file=/etc/keycloak/tls.key

If a load balancer terminates TLS, use --hostname https://auth.example.com --http-enabled true --proxy-headers xforwarded and don’t expose the /admin paths publicly (reverse proxy guide).

2. Map the tenant to a realm

Create one realm per Auth0 tenant. For signing keys, keep RS256, the default in both.

Auth0 Keycloak
Tenant Realm
Application Client (public with PKCE for SPAs and mobile, confidential for server apps)
API (audience) + permissions Client roles, client scopes, and an Audience protocol mapper
Database connection The realm’s local users
Social connection Identity provider (Google, GitHub, Apple, Microsoft, generic OIDC…)
Enterprise connection (SAML, OIDC, Azure AD) SAML or OIDC identity provider
AD/LDAP connector User federation (LDAP/Kerberos)
Rules / Actions Authentication flows, required actions, protocol mappers, or a custom Java SPI
Organizations Organizations (fully supported since Keycloak 26.0; enable per realm)
Roles Realm roles or client roles, plus groups
MFA (OTP, WebAuthn) OTP and WebAuthn policies, and conditional flows
Universal Login Login theme

Actions don’t port directly. Keycloak has no hosted JavaScript hooks. Claim enrichment becomes protocol mappers. Pre-login checks become authentication flow steps. Post-registration calls become an event listener SPI or a job that reads admin events. Anything else means writing a Java provider and deploying it to providers/.

Social connections: create each identity provider with the same Google or GitHub OAuth app, and add https://auth.example.com/realms/<realm>/broker/<alias>/endpoint to its allowed redirect URIs. Keep the Auth0 callback in place until rollback is off the table.

3. Export users from Auth0

curl -s -X POST "https://$AUTH0_DOMAIN/api/v2/jobs/users-exports" \
  -H "Authorization: Bearer $MGMT_TOKEN" -H "Content-Type: application/json" \
  -d '{"format":"json","fields":[{"name":"user_id"},{"name":"email"},{"name":"email_verified"},
       {"name":"name"},{"name":"given_name"},{"name":"family_name"},{"name":"identities"},
       {"name":"app_metadata"},{"name":"user_metadata"},{"name":"created_at"}]}'
# -> {"id":"job_abc123","status":"pending",...}

curl -s -H "Authorization: Bearer $MGMT_TOKEN" "https://$AUTH0_DOMAIN/api/v2/jobs/job_abc123"

When status is completed, the location URL holds a gzipped NDJSON file. That link is valid for 60 seconds, so fetch it straight away. Leave out connection_id to export every connection. CSV can’t include whole metadata objects, so use JSON.

4. Choose how passwords move

Auth0 hashes database passwords with bcrypt. Keycloak 26 hashes with Argon2 by default, also supports PBKDF2, and has no bcrypt support built in. You have three options:

Option How Trade-off
Import hashes Install a community bcrypt provider (for example keycloak-bcrypt) and import the hashes from the support export Nobody notices the switch. But you depend on the export arriving, and on a third-party JAR you have to rebuild and test on every Keycloak upgrade
Lazy migration A user storage SPI such as keycloak-user-migration calls a small service you write. That service checks the password with Auth0’s Resource Owner Password grant (grant_type=http://auth0.com/oauth/grant-type/password-realm, realm=Username-Password-Authentication) No hashes needed. Keycloak stores the password in its own format on first login. Auth0 must stay paid and online for the whole window, the Password grant must be enabled on a confidential client, and Auth0’s brute-force protection can block your service’s IP (read this first)
Forced reset Import users without credentials and add the UPDATE_PASSWORD required action, or send reset emails Simplest to implement. Expect lost logins and support tickets

A common combination is to import everyone, run lazy migration for 1–3 months, and then send reset emails to whoever is left.

With the bcrypt provider, a user’s credential looks like this:

{ "type": "password",
  "secretData": "{\"value\":\"$2b$10$…\",\"salt\":\"\"}",
  "credentialData": "{\"hashIterations\":10,\"algorithm\":\"bcrypt\"}" }

Check the field names against the provider’s source and test one user before the bulk run. Once that user logs in, add a realm password policy of hashAlgorithm(argon2), so Keycloak re-hashes each password on its next login.

5. Import users

Convert each NDJSON line to a Keycloak UserRepresentation. Store the old ID as an attribute (auth0_id). Link social users through federatedIdentities, so their next Google or GitHub login lands on the same account:

{ "username": "ana@example.org", "email": "ana@example.org", "emailVerified": true,
  "enabled": true, "attributes": { "auth0_id": ["auth0|64f1…"] },
  "federatedIdentities": [{ "identityProvider": "google", "userId": "1098…", "userName": "ana@example.org" }],
  "credentials": [ … ] }

Load them in batches of a few thousand through the Admin REST API’s partial import:

curl -s -X POST "https://auth.example.com/admin/realms/myrealm/partialImport" \
  -H "Authorization: Bearer $KC_ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d @users-batch-001.json   # {"ifResourceExists":"SKIP","users":[...]}

For very large sets, stop all nodes and run bin/kc.sh import --file realm.json, which writes straight to the database. You can also do a partial import from the console under Realm settings → Action → Partial import.

6. Change the apps

Auth0 Keycloak
Issuer https://example.eu.auth0.com/ (trailing slash) https://auth.example.com/realms/myrealm
Discovery /.well-known/openid-configuration /realms/myrealm/.well-known/openid-configuration
JWKS /.well-known/jwks.json /realms/myrealm/protocol/openid-connect/certs
Logout /v2/logout (or OIDC logout if enabled) end_session_endpoint from discovery
sub auth0|64f1… A Keycloak UUID
Roles / permissions Custom namespaced claim from an Action, permissions with RBAC realm_access.roles, resource_access.<client>.roles
Access token aud Your API identifier Needs an Audience mapper

7. Verify

On staging, with a copy of production users, test each path end to end:

8. Cut over gradually

Refresh tokens and sessions don’t carry over, so every user has to log in again. Announce it, and avoid your busiest day.

Move one app (client) at a time, internal tools first. For a single large app, route a share of new logins to Keycloak with a flag and leave existing Auth0 sessions alone until they expire. Turn on saving of login events (Realm settings → User events settings), then watch LOGIN_ERROR events under Events and your app’s error rate.

9. Rollback plan

Keep the Auth0 tenant, its callbacks, and the app config for both providers until the lazy-migration window ends. Before cutover, rehearse a rollback with a new user, a changed password, an MFA change and an enterprise login.

Pointing the apps back at Auth0 restores the old login path, not the current identity data. Users who signed up or changed their password in Keycloak are missing or stale in Auth0. Auth0’s bulk import has a custom_password_hash field, but test whether it accepts the hashes Keycloak stored before relying on it; the fallback is to import those users without passwords and send reset emails. Pause signups and profile changes on Keycloak while you reconcile, and include changed roles, profile data and identity links.

10. Clean up

Found a command or version mismatch? Report a correction with the version and steps to reproduce it. Remove secrets and customer data first.