Skip to main content
Stateful Auth0 emulation. The Authentication API: OIDC discovery and JWKS, /authorize with a Universal Login page and login sessions, the token endpoint (authorization code with PKCE, refresh tokens with optional rotation and reuse detection, client credentials, and the password grants), userinfo, refresh token revocation, database signup and change password, and both logout endpoints. The Management API v2: users (with Auth0’s search query syntax), roles, permissions, applications, client grants, APIs, database connections, sessions, and refresh tokens, and role based access control in access tokens. Tokens are JWTs signed with an RS256 key kept in emulator state, or with an API’s HS256 secret. The tests drive it with openid-client (6.8.8), the shared OIDC conformance suite, and the official auth0 SDK (7.5.0): its token provider and ManagementClient.

At a glance

Start

Auth0 starts only when named in config or with --service. When it is the only enabled service, it listens on http://localhost:4000. Without a seed config there are no users and only the Username-Password-Authentication database connection, and any client_id works (see Applications).

URL mapping

The issuer is the emulator’s URL with a trailing slash (http://localhost:4000/), the same shape as Auth0’s https://{tenant}.auth0.com/. Libraries that take an issuer URL work unchanged. With openid-client:
Other libraries that take an issuer URL point at the same URL. Libraries that take only a bare domain and always use https need a way to override their URLs or their fetch, as the auth0 SDK has. The official auth0 SDK builds https://{domain}/... URLs itself, so it needs a custom fetch that sends the tenant’s requests to the emulator:
The client gets its Management API token from the emulator with client credentials, then calls /api/v2/... there. See Management API.

Tenant

  • Domain. domain in the seed names the tenant as clients see it (default: the emulator’s host). It sets the Management API’s audience, https://{domain}/api/v2/. The emulator also accepts {emulator URL}/api/v2/ as the same API, and a client grant for either covers both.
  • Default audience. default_audience is the audience for token requests that name none, as the tenant setting of that name is on Auth0.
  • Default directory. default_directory names the connection the password grant signs in against. As on Auth0, a tenant has none until it is set, and the plain password grant fails with 500 server_error “Authorization server not configured with default connection.” until then.
  • Connections. Every tenant has the Username-Password-Authentication database connection (password policy good). Seed more under connections.

Applications

Clients come from clients in the seed or from the Management API. Until one is registered either way, any client_id works: the emulator makes a client for it on first use that accepts any secret, any callback URL, every grant type, and client credentials for every API with all its scopes, so a quick start needs no setup. Once a client is registered, only registered clients work, and that stays so even if every client is deleted later. Changing a client made on first use through the Management API (an update or a secret rotation) registers it, so its secret is checked from then on. New clients get Auth0’s defaults by app_type: Add password or http://auth0.com/oauth/grant-type/password-realm to grant_types for the password grants. Confidential clients may authenticate with client_secret_post or client_secret_basic. A missing or wrong secret is 401 invalid_client (the error Auth0’s docs name; the message Client authentication failed. is Baseline’s).

Universal Login

GET /authorize checks the request, then either answers from a live session or stores the request and redirects to /u/login?state=..., as Auth0 does. The login page lists the users who may log in to the client (users on connections turned on for it; with connection, only that connection’s users; login_hint puts the matching user first). Choosing one logs them in, starts a session, and sends the code to the callback. Blocked users are listed but get error=unauthorized with user is blocked. Parameters: client_id, redirect_uri, response_type=code, scope, audience, state, nonce, code_challenge and code_challenge_method (S256 or plain), response_mode (query, fragment, or form_post), connection, prompt (none or login), max_age, and login_hint.
  • Callback. Without redirect_uri the client’s first callback URL is used. A redirect_uri outside the client’s callback URLs shows an error page (Callback URL mismatch), as Auth0 does. URLs are compared without the query string and a trailing slash.
  • Errors after the callback is trusted go back to it with error, error_description, and state: invalid_request for a missing response_type, a bad code_challenge_method, or an unknown connection; unsupported_response_type for anything but code; unauthorized_client when the client lacks the authorization_code grant; access_denied with Service not found: {audience} for an unknown API.
  • Scopes. OpenID Connect scopes stay; an API’s scopes stay when the API defines them; other scopes are dropped, as Auth0 drops them. offline_access stays only when a refresh token can be issued: the client has the refresh_token grant and the API allows offline access (allow_offline_access), or the request names no API.
  • Sessions. A login sets the auth0 cookie. While the session lives, /authorize signs the user in again without the login page. prompt=login shows the page anyway; prompt=none answers login_required (Login required) without a session; max_age treats an older login as no session and puts auth_time in the ID token. Sessions end after 3 days idle or 30 days in all, Auth0’s defaults.
  • Codes are single use and last 10 minutes (Auth0 says only that codes are short lived).

Token endpoint

POST /oauth/token takes a form or a JSON body. Errors are {error, error_description}: The messages for a wrong redirect_uri (Invalid redirect_uri), a refresh scope wider than the grant (403 invalid_scope), an unknown realm, and a connection turned off for the client are Baseline’s.

Tokens

  • Access tokens are JWTs with iss, sub (the user’s ID, or {client_id}@clients for client credentials), aud, iat, exp, azp, scope, and jti, plus gty (client-credentials or password) for those grants. The API decides how they are signed: signing_alg: RS256 (the default for seeded APIs) uses the tenant key in the JWKS; HS256 uses the API’s signing_secret, with no kid. An API with token_dialect: rfc9068_profile (or rfc9068_profile_authz) gets tokens in the RFC 9068 profile: the header says typ: at+jwt, and client_id replaces azp and gty. With an API the audience is the API’s identifier, joined by {issuer}userinfo when openid was granted. They last the API’s token_lifetime (default 86400 seconds), or its token_lifetime_for_web (default 7200) for single page apps. Without an API, Auth0 issues an opaque token for /userinfo; Baseline signs a JWT whose audience is {issuer}userinfo, lasting 86400 seconds.
  • ID tokens carry sub, aud (the client ID), sid (for logins through /authorize), nonce, and the profile claims the scopes allow: profile adds name, nickname, picture, updated_at, given_name, and family_name; email adds email and email_verified. They last the client’s id_token_lifetime (default 36000 seconds).
  • The token response has access_token, id_token (with openid), refresh_token (with offline_access), scope, expires_in, and token_type: "Bearer".

Signing key

Tokens are signed with an RSA key stored in emulator state (kid: baseline-auth0-1), so they stay valid across snapshot restores and persisted restarts. Set auth0.signing_key.private_jwk to an RSA private JWK to pin the key. See OIDC signing keys.

Refresh tokens

  • Non-rotating (the default): the token keeps working until revoked and never expires; a refresh answers without a new refresh token.
  • Rotating (refresh_token.rotation_type: rotating): each refresh returns a new token and retires the old one. Using a retired token again is taken as theft and revokes the whole family, including the newest token, as Auth0’s automatic reuse detection does. With refresh_token.leeway (seconds), the token most recently retired still works inside that window. A family expires token_lifetime seconds after its first token (default 30 days); rotation does not extend it.
  • Idle lifetime. An expiring token (expiration_type: expiring) that goes unused for idle_token_lifetime seconds stops working, unless infinite_idle_token_lifetime is set. Rotating clients default to 15 days; each use starts the clock again.
  • Switching rotation. Turning rotation on makes the next refresh of a non-rotating token return a rotating one and delete the grant’s non-rotating tokens; turning it off swaps a rotating token for a non-rotating one and revokes the family, as Auth0 migrates them.
  • Revocation. POST /oauth/revoke with client_id, the client’s secret, and token revokes the token (and its rotation family) and answers 200 with no body. Unknown tokens and other clients’ tokens answer 200 without effect. With revoke_refresh_token_grant: true it revokes every token of the same user, client, and audience, as the tenant setting “Refresh Token Revocation Deletes Grant” does.

Client credentials

A client calls an API with client credentials only through a client grant (client_grants in the seed, or created through the Management API), and gets the grant’s scopes, or the subset it asks for. A grant with allow_all_scopes covers every scope the API defines, including scopes added later. Only grants with subject_type: client (the default) count; user grants are kept but open no flow here. The token has no refresh token.

Userinfo

GET /userinfo with Authorization: Bearer {access token} returns sub and the profile claims the token’s scopes allow, with the user’s current values. The token must be for /userinfo and carry openid; otherwise, and for blocked or deleted users, the answer is 401 with the body Unauthorized and a WWW-Authenticate: Bearer challenge, as Auth0 sends.

Database connections

  • POST /dbconnections/signup — Sign up: with connection, email, password, and optional client_id, username (only for connections with requires_username), given_name, family_name, name, nickname, picture, phone_number, and user_metadata (at most 10 string properties). Returns {_id, email_verified: false, email, ...}, where _id is the user ID without auth0|. New users get Auth0’s defaults: name is the email, nickname its local part, and picture a Gravatar URL.
  • POST /dbconnections/change_password — Change password: with connection, email, and optional client_id. Answers We've just sent you an email to reset your password. whether or not the user exists. Baseline sends no email; the request appears in the inspector.
An existing user is 400 with code user_exists (The user already exists.). A password that fails the connection’s policy is 400 PasswordStrengthError with Auth0’s rule list in description.rules and the policy text in policy. Policies: none, low (6 characters), fair (8 characters with lower case, upper case, and numbers), good (8 characters with three of lower case, upper case, numbers, and special characters), and excellent (10 characters with three of the four, and no character three times in a row). Other validation errors use Auth0’s shape (name, code, description, statusCode) with Baseline’s codes and messages. A connection with disable_signup refuses with operation_not_enabled.

Logout

  • GET /v2/logout — Log out: ends the session and redirects to returnTo, which must be one of the client’s allowed logout URLs (with client_id) or the tenant’s (allowed_logout_urls in the seed, without one). Without returnTo it goes to the client’s first allowed logout URL, or shows a logged out page.
  • GET /oidc/logout — Log out with OIDC: OpenID Connect RP-initiated logout, also as POST /oidc/logout with form fields. It takes id_token_hint, logout_hint, client_id, post_logout_redirect_uri, and state, which is passed back on the redirect. An expired ID token is accepted as a hint. A hint from another tenant, a client_id that does not match the hint’s audience, or a logout_hint that does not match its session is refused. Without a hint naming the session, the user confirms on a page first, as Auth0 asks.

Management API

The Management API lives under /api/v2. Requests carry Authorization: Bearer {access token}, a token this tenant signed for the Management API audience (https://{domain}/api/v2/ or {emulator URL}/api/v2/). A machine gets one with client credentials and a client grant for that audience, whose scopes decide what it may call. Each endpoint needs the scopes Auth0 lists for it, such as read:users or create:roles. Errors use the Management API’s body, { statusCode, error, message, errorCode }: 401 bodies about the token repeat the message under attributes.error. Body validation follows the published request schemas: required properties, types, lengths, the email format, the phone number pattern, and no unknown properties (updates to users let unknown properties through and ignore them, as their schema allows). Validation messages use the JSON Schema wording the Management API reports, without the property descriptions it appends. Lists page with page (from 0), per_page (up to 100, default 50), and include_totals, which wraps the page in { start, limit, total, ... }. GET /roles, GET /roles/{id}/users, GET /clients, GET /client-grants, and GET /connections also take checkpoint paging (from and take), answering { ..., next } while more remain. Sessions, refresh tokens, and a connection’s clients page by checkpoint only.

Users

Users come back in Auth0’s shape: user_id, email, email_verified, name, nickname, picture, identities ([{ connection, user_id, provider: "auth0", isSocial: false }]), created_at, updated_at, blocked, and, once set, username, given_name, family_name, phone_number, user_metadata, app_metadata, last_password_reset, and after a first login logins_count, last_login, and last_ip. Baseline always includes blocked. Errors: an unknown connection is 400 The connection does not exist. (inexistent_connection); an existing user (same email or username on the connection, or the same user_id) is 409 The user already exists. (auth0_idp_error); a weak password is 400 PasswordStrengthError: Password is too weak; a username on a connection without usernames is 400 Cannot set username for connection without requires_username; an update with both email and password is 400 Cannot update password and email simultaneously; an email or username another user has is 400 The specified new email already exists (or username). q takes the Lucene syntax Auth0’s user search documents:
  • Fields: user_id, email, email.domain, email_verified, name, given_name, family_name, nickname, username, phone_number, blocked, logins_count, created_at, updated_at, last_login, last_ip, last_password_reset, identities.connection, identities.provider, identities.user_id, and paths into user_metadata and app_metadata (arrays are stepped through, so user_metadata.addresses.cities:"Paris" finds a city in a list).
  • Terms match whole values: name:"Ada Lovelace", blocked:true, logins_count:3. email, email.domain, name, given_name, family_name, and nickname ignore case; other fields do not. A term without a field searches user_id, email, name, nickname, given_name, family_name, and username.
  • Wildcards * and ?: email:test*, name:*example*. A leading wildcard needs at least 3 other characters.
  • Ranges: logins_count:[10 TO 100}, last_login:[2025-12-01 TO 2025-12-31], logins_count:[100 TO *]. A bare date covers its whole day.
  • _exists_:field, and the operators AND, OR, NOT (also &&, ||, !, and a leading -) with parentheses and field groups (name:("Ada" OR "Sam")). Terms side by side are joined with OR.
As Auth0 documents, user_metadata takes no wildcards or ranges, null values and empty arrays or objects are not searchable, and users who never logged in have no logins_count. Sorting by metadata is refused. Syntax errors are 400 with errorCode: invalid_query_string and Baseline’s messages.

Roles and permissions

Role IDs look like rol_ and 16 characters. Organization roles are not emulated, so type: organization is refused and owner_id filters find nothing. Messages for an unknown API or scope in a permission list are Baseline’s.

Clients

New clients get the defaults in Applications. What a caller sees depends on its scope, as Auth0 documents: client_id, name, description, and app_type with any read scope; everything else with read:clients or read:client_keys; client_secret only with read:client_keys or read:client_credentials. Changing client_secret needs update:client_keys, and other settings update:clients or update:client_keys. Settings Baseline does not act on (logo_uri, oidc_conformant, addons, mobile, and the rest of the documented list) are kept and returned. Every app_type the API lists is accepted; only spa, native, and non_interactive change behavior. ID tokens are always RS256, so jwt_configuration.alg other than RS256 is refused, as are multi-resource refresh token policies.

Client grants

Grants come back as { id, client_id, audience, scope, allow_any_organization, is_system: false, subject_type }, with allow_all_scopes and organization_usage when set. IDs look like cgr_ and 16 characters. Grants for third party clients by default (default_for) and anonymous_user grants are not emulated. Messages for scopes the API does not define are Baseline’s.

Resource servers

As on Auth0, an API created through the Management API signs with HS256 unless signing_alg says otherwise (seeded APIs default to RS256), and every API gets a signing_secret. RS512 and PS256 are refused. token_lifetime_for_web starts at the smaller of 7200 and token_lifetime. Replacing scopes takes removed scopes away from every role, user, and client grant that held them. Changing enforce_policies or token_dialect applies to the next token. The Management API itself cannot be changed or deleted here (403, errorCode operation_not_supported).

Connections

The options Baseline acts on are passwordPolicy (none, low, fair, good, or excellent; good when left out), requires_username, and disable_signup; other options are kept. options appear only for callers with read:connections_options, and changing them needs update:connections_options, as the OAS describes. A connection created through the API is on for no clients until enabled_clients or the clients endpoint turns it on; seeded connections without enabled_clients are on for every client, and their client list spells that out. Only database connections (strategy: auth0) are emulated; other strategies are refused.

Sessions

A session comes back with id (the sid in ID tokens), user_id, created_at, updated_at, authenticated_at, last_interacted_at, idle_expires_at and expires_at (from the tenant’s session lifetimes), device (the user agent and IP address of the login), clients, and authentication.methods (pwd). Expired sessions are not listed. Ending a session signs its browser out of Universal Login.

Refresh tokens

A refresh token comes back with id, user_id, client_id, session_id (the session it was issued in, or null), rotating, created_at, resource_servers ([{ audience, scopes }]), and when they apply expires_at, idle_expires_at, last_exchanged_at, and refresh_token_metadata. A rotating token keeps one record and one id across rotations; revoking it revokes the family.

Tokens for the current user

A user can get a Management API token through a login flow (audience set to the Management API). Auth0 lists the scopes such a token may ask for: read:current_user, update:current_user_metadata, create:current_user_metadata, delete:current_user_metadata, update:current_user_identities, and the device credential pair. Baseline keeps those and drops any other. With them the user may read their own record and change their own user_metadata; acting on anyone else is 403 User to be acted on does not match subject in bearer token. A machine token with update:users_app_metadata may change app_metadata and user_metadata only.

Role based access control

An API with enforce_policies: true limits user tokens to the permissions the user holds, directly or through roles: the access token’s scope (and the token response’s) keeps the API scopes that were asked for and are held, and OpenID Connect scopes as before. With token_dialect: access_token_authz the token also carries every held permission for that API in a permissions claim. Permissions are read when each token is issued, so changing a user’s roles applies on their next refresh. Client credentials tokens for such an API carry their granted scopes as permissions. APIs without enforce_policies are unaffected.

Seed config

  • default_audience: optional
  • display_name: the name when left out
  • password_policy: none, low, fair, good, excellent
  • enabled_clients: every client when left out
  • roles: by name or id
  • enforce_policies: role based access control
  • token_dialect: adds the permissions claim; or rfc9068_profile(_authz)
  • signing_alg: or HS256, signed with signing_secret
  • app_type: regular_web, spa, native, non_interactive
  • signing_secret (not set above, for example a-secret-of-16-characters-or-more).
  • allow_all_scopes (not set above, for example true): every scope the API defines, now and later
Seeded users have email_verified: true unless the seed says otherwise. Seeded client IDs and secrets, and role IDs, are generated when left out. A user naming a role the seed does not define stops the emulator from starting.

Inspector

/_inspector shows users with their roles, database connections, and change password requests; applications (seeded or registered, or made on first use) and client grants; APIs (with their signing algorithm, token dialect, and access control setting) and tenant settings; roles with their permissions and member counts, and permissions assigned directly; and sessions and refresh tokens with their rotation status.

Limits

  • The Management API serves users, roles, permissions, clients, client grants, resource servers, connections, sessions, and refresh tokens. Logs, tickets, organizations, actions, custom domains, tenant settings, and the rest are not served.
  • Only response_type=code is supported: no implicit or hybrid flows.
  • Not emulated: the device flow, passwordless, MFA, organizations, social and enterprise connections, Actions and Rules (so no custom claims), consent screens (every client is first party), HS256 ID tokens, RS512 and PS256 access tokens, private key JWT client authentication, DPoP, and rate limits.
  • Change password requests are recorded, not emailed, and have no reset page.

Conformance

See CONFORMANCE.md for every provider.