Lazy Migration from Auth0
Move traffic from an Auth0 tenant to AuthHero one user at a time, without forcing every user to reset their password or re-authenticate. Two flows handle the long tail:
- Password fallback — on a password login, if no local hash matches, AuthHero verifies the password against the upstream Auth0 (Resource Owner Password Realm grant), fetches the user's profile from
/userinfo, creates the user locally, and stores the hash. Subsequent logins are served entirely by AuthHero. - Refresh-token proxy — Auth0 refresh tokens are opaque server-side handles, so they can't be re-issued by AuthHero. Instead, refresh-token requests that don't match a local row are forwarded to upstream Auth0 verbatim. Existing Auth0 sessions keep working until the user does an interactive login (where the password fallback migrates them).
Bulk import via /api/v2/users-imports and the Auth0 proxy app remain useful for other migration shapes; this page covers the lazy/just-in-time approach that needs no client SDK changes.
What you need from Auth0
A regular Auth0 application (typically your existing one) with these settings:
- Grant Types: enable
Passwordand the extension granthttp://auth0.com/oauth/grant-type/password-realmunder Application → Settings → Advanced → Grant Types. - Client ID and Client Secret of that application — AuthHero uses them to call upstream
/oauth/tokenfor both password-realm grants and refresh-token grants. - The Auth0 Domain (e.g.
example.auth0.com).
No Management API M2M token is required for v1 — /userinfo is called with the access_token returned by the password-realm grant.
Configuration
Configure the upstream connection through the standard connections API/admin UI — no new endpoints, no new tenant fields.
1. Create the upstream-source connection
Add a connection with strategy auth0. This connection is metadata only; users never authenticate against it directly (it is filtered out of the universal-login button list).
In the react-admin admin UI:
- Connections → New connection
- Strategy → Auth0 (migration source)
- Open the new connection and fill in:
- Token Endpoint —
https://<your-tenant>.auth0.com/oauth/token - Userinfo Endpoint —
https://<your-tenant>.auth0.com/userinfo - Client ID / Client Secret — from the Auth0 application above
- Proxy Refresh Tokens — enable to forward unknown refresh tokens to upstream Auth0
- Token Endpoint —
Or via the Management API:
POST /api/v2/connections
Content-Type: application/json
{
"name": "auth0-source",
"strategy": "auth0",
"options": {
"token_endpoint": "https://example.auth0.com/oauth/token",
"userinfo_endpoint": "https://example.auth0.com/userinfo",
"client_id": "<auth0-client-id>",
"client_secret": "<auth0-client-secret>",
"import_mode": true
}
}A tenant is expected to have at most one strategy: "auth0" connection. The import_mode flag on this connection controls refresh-token proxying specifically.
2. Enable lazy import on the database connection
To let password logins fall back to upstream Auth0, set import_mode: true on the DB connection (typically Username-Password-Authentication):
In the admin UI: edit the connection and toggle Import Mode ("On unknown passwords, fall back to upstream Auth0…").
Or via the Management API:
PATCH /api/v2/connections/<db-connection-id>
Content-Type: application/json
{
"options": { "import_mode": true }
}import_mode here is the same flag Auth0 uses on its own Custom Database connections — same name, symmetric semantics. AuthHero's password flow will:
- Try the local password hash first.
- On miss, look up the tenant's
strategy: "auth0"connection for upstream credentials. - Call
POST /oauth/tokenwithgrant_type=http://auth0.com/oauth/grant-type/password-realm,realm=<DB connection name>, the supplied username/password, and the upstream client credentials. - On 200, fetch the profile from
/userinfoand create the local user (if missing) + bcrypt hash. - On any upstream error, surface the existing
INVALID_PASSWORDrejection so the upstream's existence is not leaked.
What happens during a typical migration
| Day | Event | What runs |
|---|---|---|
| 0 | DNS flipped, AuthHero is now serving auth | Local lookups miss; password/RT fallback activates |
| 0–N | Active users open the app | RT proxy forwards their refresh requests to Auth0; they keep their session |
| 0–N | A user signs in with username/password | Password fallback verifies against Auth0, creates them locally, stores hash |
| N+1 | Migrated user signs in again | Served entirely from AuthHero — no upstream call |
| Eventually | Last Auth0 session expires | RT proxy traffic drops to zero |
Once the upstream traffic drops to a handful per day you can flip import_mode off and decommission the upstream Auth0 tenant.
Edge cases and gotchas
- MFA-enforced users: Auth0 returns
mfa_requiredfrom the password-realm grant. AuthHero treats it as a genericINVALID_PASSWORDto avoid leaking that the user exists upstream — affected users must reset on the AuthHero side. unauthorized_client: Grant type … not allowed: the Auth0 application has not been granted the password-realm grant. Enable it under Application → Advanced → Grant Types.- Failed-login throttling still applies: the existing 3-strikes lockout fires whether the password compare runs locally or against upstream, so an attacker can't bypass it by forcing the upstream path.
- Refresh-token rotation: when the Auth0 tenant has rotation enabled, the proxied response contains a new refresh token; AuthHero relays it to the client unchanged. AuthHero does not persist upstream refresh tokens.
- Connection name === realm: AuthHero sends the local DB connection's name as the upstream
realm. Keep the DB connection's name aligned with the upstream connection name (typicallyUsername-Password-Authentication). - Connection visibility: the
strategy: "auth0"connection is automatically hidden from the universal-login button list — it's a metadata holder, not an interactive auth path.
Comparison with the other migration mechanisms
- Auth0 proxy app — a thin reverse proxy that exposes an Auth0-shaped surface to legacy clients during the cutover. Use when clients can't be repointed to AuthHero yet.
- Token Exchange (RFC 8693) — proposed: lets clients explicitly swap a long-lived Auth0 refresh token for a native AuthHero refresh token. Cleaner end state but requires an SDK change.
- Lazy migration (this page) — no client changes; suitable when you cannot or do not want to update apps. Refresh tokens stay proxied until the user re-logs in.
These are complementary — most production migrations use lazy migration as the foundation and add the Auth0 proxy app for the small number of clients that need an Auth0-shaped HTTP surface.