Migration

Migrating from Auth0

Move an application and its users from Auth0 to auth-service with the Auth0 compatibility profile and the migrate:auth0 command, keeping ids and passwords.

Auth0 is the easiest source to move: its user export carries the bcrypt password hashes, so users keep their password, and ids can be kept as the token sub. The Auth0 compatibility profile takes care of how SDKs and APIs talk to the service.

What moves, and what does not

In Auth0In auth-serviceNotes
Application (regular web, SPA, native)Application (confidential or isPublic)Created again
Machine-to-machine applicationConfidential applicationclient_credentials, scope m2m
API and its identifier (audience)Application urlaudience is accepted as resource with the profile
UsersUsers, with their auth0|… id as idsub unchanged
Passwords (bcrypt)Imported hashes, replaced by native ones at the first sign-inNeeds the export with hashes
Roles and permissionsRoles; permissions per rolepermissions claim is native
Actions, Rules, hooks—No equivalent: custom claims use claim mappings
MFATOTP with backup codesUsers enroll again
Social connectionsGoogle and GitHub
OrganizationsOrganizationsRecreate and add members

1. Configure the instance

COMPAT_PROFILE=auth0

For all applications of the instance:

  • confidential clients authenticate with client_secret_post (the secret in the body), Auth0's default;
  • tokens are signed with RS256;
  • audience is accepted as resource; without either, the access token is opaque, as with Auth0;
  • /authorize, /oauth/token, /oauth/revoke, /userinfo and /.well-known/jwks.json answer like the canonical endpoints, so a tenant domain setting can point to the service.

Auth0 puts roles in a namespaced claim. Add yours if your APIs read it, for example:

CLAIM_MAPPINGS=[{"claim":"https://acme.example/roles","source":"roles","scope":"roles"}]

Set the profile before the first start. Details: Compatibility profiles.

2. What still changes in an application

Auth0auth-service
Domain / issuerhttps://<tenant>.auth0.com/https://auth.example.com (no trailing slash)
client_idGeneratedThe application slug
Audienceaudience of the APIaudience is accepted as resource (the profile turns it on); aud is that resource, which must be the application's own url or the url of another application listed in its allowedResources. With no audience, the token is opaque
auth0-spa-jsJSON or form token requestsForm requests only: keep the default useFormData
Logout/v2/logout?returnTo=Local sign-out in the app, or end-session (global, off by default per application)
subauth0|…Unchanged

The issuer of tokens is the root URL: configure your API with https://auth.example.com, not with the Auth0 form.

3. Move the users

Export the users from Auth0 with their password hashes (a bulk export; the hashes need a support request on Auth0's side), then:

# Dry run
pnpm migrate:auth0 -- --file users.ndjson --app acme-app

# For real
AUTH_SERVICE_URL=https://auth.example.com ADMIN_EMAIL=… ADMIN_PASSWORD=… \
  pnpm migrate:auth0 -- --file users.ndjson --app acme-app --apply

The file is a JSON array or one JSON object per line. The command reads user_id, email, email_verified, name (or nickname), passwordHash (a bcrypt hash) and app_metadata.roles; roles become the application's roles. Users without an email are skipped. Options (--role-priority, --trust-email, --send-verification, --no-keep-ids, --report), the environment variables, the superadmin requirement and the report are those of the Okta command; --trust-email is true by default here and marks as verified the addresses that the export says are verified (email_verified).

Users without a hash (social-only accounts, for instance) get a random temporary password that is not shown or reported: they set their own with Forgot your password?. Users with a hash keep their password, which is re-hashed natively at their first sign-in. Delete the export file when you are done: it contains password hashes.

4. Cut over

Accept both issuers in your APIs while users move, as shown in the Okta guide: sub is the same on both sides. Then switch the sign-in side one application at a time, watch the login history, and retire Auth0 when no traffic remains.

Copyright © 2026