> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth0

> Universal Login, OAuth and OIDC, Management API, role-based access control.

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`](https://github.com/panva/openid-client) (6.8.8), the shared OIDC conformance suite, and the official [`auth0`](https://github.com/auth0/node-auth0) SDK (7.5.0): its token provider and `ManagementClient`.

## At a glance

| | |
| - | - |
| Package | `@deploybaseline/auth0` |
| Port | `4000` when started alone; named services take consecutive ports |
| Config key | `auth0` |
| HTTP operations | 72, browsable at `http://localhost:3999/docs/auth0` while it runs ([API reference](/api-reference.md)) |

```bash CLI theme={null}
npx @deploybaseline/cli start --service auth0
```

```ts Programmatic theme={null}
import { createEmulator } from "@deploybaseline/cli";

const auth0 = await createEmulator({ service: "auth0" });
// auth0.url is http://localhost:4000
```

```json baseline.config.json theme={null}
{
  "auth0": {
    "domain": "baseline.us.auth0.com",
    "default_directory": "Username-Password-Authentication",
    "users": [
      {
        "email": "ada@example.com",
        "password": "Passw0rd!",
        "name": "Ada Lovelace"
      }
    ],
    "apis": [
      {
        "identifier": "https://api.example.com",
        "name": "Example API",
        "scopes": ["read:items", "write:items"],
        "allow_offline_access": true
      }
    ],
    "clients": [
      {
        "client_id": "baseline-web-app",
        "client_secret": "baseline-web-secret",
        "name": "Web app",
        "callbacks": ["http://localhost:3000/callback"],
        "allowed_logout_urls": ["http://localhost:3000"],
        "grant_types": ["authorization_code", "refresh_token", "password"]
      },
      {
        "client_id": "baseline-m2m",
        "client_secret": "baseline-m2m-secret",
        "name": "Backend service",
        "app_type": "non_interactive"
      }
    ],
    "client_grants": [
      {
        "client_id": "baseline-m2m",
        "audience": "https://api.example.com",
        "scope": ["read:items"]
      }
    ]
  }
}
```

## Start

```bash theme={null}
npx @deploybaseline/cli start --service auth0
```

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](#applications)).

## URL mapping

| Real Auth0 URL | Emulator URL |
| - | - |
| `https://{tenant}.auth0.com/.well-known/openid-configuration` | `$AUTH0_EMULATOR_URL/.well-known/openid-configuration` |
| `https://{tenant}.auth0.com/.well-known/jwks.json` | `$AUTH0_EMULATOR_URL/.well-known/jwks.json` |
| `https://{tenant}.auth0.com/authorize` | `$AUTH0_EMULATOR_URL/authorize` |
| `https://{tenant}.auth0.com/u/login` | `$AUTH0_EMULATOR_URL/u/login` |
| `https://{tenant}.auth0.com/oauth/token` and `/oauth/revoke` | `$AUTH0_EMULATOR_URL/oauth/token` and `/oauth/revoke` |
| `https://{tenant}.auth0.com/userinfo` | `$AUTH0_EMULATOR_URL/userinfo` |
| `https://{tenant}.auth0.com/dbconnections/...` | `$AUTH0_EMULATOR_URL/dbconnections/...` |
| `https://{tenant}.auth0.com/v2/logout` and `/oidc/logout` | `$AUTH0_EMULATOR_URL/v2/logout` and `/oidc/logout` |
| `https://{tenant}.auth0.com/api/v2/...` | `$AUTH0_EMULATOR_URL/api/v2/...` |

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`:

```ts theme={null}
import * as client from "openid-client";

const config = await client.discovery(
  new URL(`${process.env.AUTH0_EMULATOR_URL}/`),
  "baseline-web-app",
  "baseline-web-secret",
  undefined,
  { execute: [client.allowInsecureRequests] }, // the emulator speaks plain http
);
```

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:

```ts theme={null}
import { ManagementClient } from "auth0";

const domain = "baseline.us.auth0.com"; // the seed's `domain`
const emulator = process.env.AUTH0_EMULATOR_URL!;
const toEmulator: typeof fetch = (input, init) => {
  const url = new URL(input instanceof Request ? input.url : String(input));
  return fetch(url.host === domain ? new URL(url.pathname + url.search, emulator) : input, init);
};

const management = new ManagementClient({
  domain,
  clientId: "baseline-m2m",
  clientSecret: "baseline-m2m-secret",
  fetch: toEmulator,
});
```

The client gets its Management API token from the emulator with client credentials, then calls `/api/v2/...` there. See [Management API](#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](#clients). 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`:

| `app_type` | Grant types | `token_endpoint_auth_method` |
| - | - | - |
| `regular_web` (default) | `authorization_code`, `implicit`, `refresh_token`, `client_credentials` | `client_secret_post` |
| `spa` | `authorization_code`, `implicit`, `refresh_token` | `none` |
| `native` | `authorization_code`, `implicit`, `refresh_token`, `client_credentials` | `none` |
| `non_interactive` | `client_credentials` | `client_secret_post` |

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.

| Grant | Notes |
| - | - |
| `authorization_code` | `code`, `redirect_uri` (required when `/authorize` named one, and must match), `code_verifier` when the request used PKCE. Public clients must have used PKCE |
| `refresh_token` | `refresh_token`, optional `scope` to narrow the new access token |
| `client_credentials` | `audience` (or the tenant's default), optional `scope` |
| `password` | `username` (email or username), `password`, optional `audience` and `scope`; signs in against the default directory |
| `http://auth0.com/oauth/grant-type/password-realm` | Same, against the connection named by `realm` |

Errors are `{error, error_description}`:

| Case | Status | Error |
| - | - | - |
| No `grant_type` | 400 | `invalid_request`, `Missing required parameter: grant_type` |
| Unknown grant type | 400 | `unsupported_grant_type` |
| Client lacks the grant | 403 | `unauthorized_client`, `Grant type '{grant}' not allowed for the client.` |
| Wrong or missing secret | 401 | `invalid_client` |
| Bad, used, expired, or another client's code | 403 | `invalid_grant`, `Invalid authorization code` |
| Wrong PKCE verifier | 403 | `invalid_grant`, `Failed PKCE verification` |
| Unknown, revoked, or expired refresh token | 403 | `invalid_grant`, `Unknown or invalid refresh token.` |
| No audience for client credentials | 403 | `access_denied`, `No audience parameter was provided, and no default audience has been configured` |
| Unknown API for client credentials | 403 | `access_denied`, `Service not enabled within domain: {audience}` |
| No client grant for the API | 403 | `access_denied`, `Client is not authorized to access "{audience}". You need to create a "client-grant" associated to this API. ...` |
| Scopes outside the client grant | 403 | `access_denied`, `Client has not been granted scopes: ...` |
| Wrong email or password | 403 | `invalid_grant`, `Wrong email or password.` |
| Blocked user | 401 | `unauthorized`, `user is blocked` |

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](/configuration.md#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](#client-grants)), 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 }`:

| Case | Status | Message |
| - | - | - |
| No `Authorization` header | 401 | `Missing authentication` |
| Not a bearer token | 401 | `Bad HTTP authentication header format` |
| Expired token | 401 | `Expired token received for JSON Web Token validation` |
| Signed by another key | 401 | `Invalid signature received for JSON Web Token validation` |
| Not a JWT, or for another audience or issuer | 401 | `Invalid token` |
| Missing scope | 403 | `Insufficient scope, expected any of: read:users` (errorCode `insufficient_scope`) |
| Invalid body | 400 | `Payload validation error: 'Missing required property: connection'.` (errorCode `invalid_body`) |
| Invalid query | 400 | `Query validation error: 'Value 101 is greater than maximum 100' on property per_page.` (errorCode `invalid_query_string`) |
| Unknown user or role | 404 | `The user does not exist.` (`inexistent_user`), `The role does not exist.` (`inexistent_role`) |
| Unknown client, grant, or API | 404 | `Client not found.` (`inexistent_client`), `The client grant does not exist.` (`inexistent_grant`), `Resource server not found.` (`inexistent_resource_server`) |
| Unknown connection, session, or refresh token | 404 | `The connection does not exist.` (`inexistent_connection`), `The session does not exist.` (`inexistent_session`), `The refresh token does not exist.` (`inexistent_refresh_token`) |

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

| Endpoint | Notes | |
| - | - | - |
| `GET /api/v2/users` | `q` ([search](#user-search)), `sort` (`field:1` or `field:-1`), `fields` and `include_fields`, paging. With `include_totals` the body also has `length`. Paging stops at the first 1000 results, as Auth0's does | |
| `GET /api/v2/users-by-email` | `email`, compared exactly (database users' emails are stored in lower case), `fields` | |
| `POST /api/v2/users` | `connection` (required), `email`, `password`, `username` (only, and then required, on connections with `requires_username`), `user_id` (becomes \`auth0 | {user_id}`), profile fields, `blocked`, `email\_verified`, `phone\_number`, `user\_metadata`, `app\_metadata`. `201\` with the user |
| `GET /api/v2/users/{id}` | `fields` and `include_fields` | |
| `PATCH /api/v2/users/{id}` | Fields replace their values and `null` removes them; `user_metadata` and `app_metadata` merge one level deep, and a `null` key removes that key. A new `email` is unverified unless `email_verified` comes with it. A new `password` must meet the connection's policy and sets `last_password_reset` | |
| `DELETE /api/v2/users/{id}` | `204`, also for unknown users. Ends the user's sessions and refresh tokens and drops their roles and permissions | |

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`).

### User search

`q` takes the Lucene syntax Auth0's [user search](https://auth0.com/docs/manage-users/user-search/user-search-query-syntax) 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

| Endpoint | Notes |
| - | - |
| `GET`, `POST /api/v2/roles` | `name_filter` (case insensitive, part of the name), paging or checkpoints. Create takes `name` and `description`; names are unique (`409 The role already exists.`) |
| `GET`, `PATCH`, `DELETE /api/v2/roles/{id}` | Deleting a role takes it from its users |
| `GET`, `POST`, `DELETE /api/v2/roles/{id}/permissions` | `{ permissions: [{ resource_server_identifier, permission_name }] }`; the API and its scope must exist |
| `GET`, `POST /api/v2/roles/{id}/users` | `{ users: [user_id, ...] }`; members come back as `{ user_id, picture, name, email }` |
| `GET`, `POST`, `DELETE /api/v2/users/{id}/roles` | `{ roles: [role_id, ...] }` |
| `GET`, `POST`, `DELETE /api/v2/users/{id}/permissions` | Permissions assigned directly. The list holds the user's direct and role permissions, each with `sources`: `{ source_type: "DIRECT" }` or `{ source_type: "ROLE", source_id, source_name }` |

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

| Endpoint | Notes |
| - | - |
| `GET /api/v2/clients` | `app_type` (comma separated), `is_first_party`, `is_global` (there is no global client, so `true` finds none), `q` (`client_grant.allow_any_organization:true`; `client_grant.organization_id` finds none), `fields` and `include_fields`, paging or checkpoints |
| `POST /api/v2/clients` | `name` (required, no `<` or `>`), `description` (140 characters), `app_type`, URLs, `grant_types`, `token_endpoint_auth_method`, `is_first_party`, `jwt_configuration`, `refresh_token`, `client_metadata` (10 string values), `client_secret` (generated when left out), and the other documented settings. `201` with the client and its secret |
| `GET /api/v2/clients/{id}` | `fields` and `include_fields` |
| `PATCH /api/v2/clients/{id}` | At least one property. `client_metadata` merges and a `null` value removes a key; `jwt_configuration` merges; `refresh_token` replaces the settings named, and switching `rotation_type` brings that type's defaults |
| `DELETE /api/v2/clients/{id}` | `204`, also for unknown clients. Deletes the client's grants, refresh tokens, and pending codes, and turns it off on every connection |
| `POST /api/v2/clients/{id}/rotate-secret` | A new secret; the old one stops working at once |

New clients get the defaults in [Applications](#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

| Endpoint | Notes |
| - | - |
| `GET /api/v2/client-grants` | `client_id`, `audience`, `subject_type`, and `allow_any_organization` filters, paging or checkpoints |
| `POST /api/v2/client-grants` | `client_id`, `audience` (an API identifier, or the Management API), `scope` (scopes the API defines), `allow_all_scopes`, `subject_type` (`client` or `user`), and the organization settings, which are kept. One grant per client, API, and subject type (`409`) |
| `GET`, `PATCH`, `DELETE /api/v2/client-grants/{id}` | Updates take `scope` (`null` empties it), `allow_all_scopes`, and the organization settings. Changes apply to the next client credentials token |

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

| Endpoint | Notes |
| - | - |
| `GET /api/v2/resource-servers` | Every API, starting with the Auth0 Management API (`is_system: true`). `identifiers` (repeat it, up to 10) filters by identifier |
| `POST /api/v2/resource-servers` | `identifier` (required, unique), `name`, `scopes` (`[{ value, description }]`), `signing_alg`, `signing_secret` (16 characters at least), `allow_offline_access`, `token_lifetime`, `token_dialect`, `enforce_policies`, `skip_consent_for_verifiable_first_party_clients`, and the other documented settings, which are kept |
| `GET /api/v2/resource-servers/{id}` | The id, or the API's identifier (URL encoded) |
| `PATCH /api/v2/resource-servers/{id}` | Anything but the identifier. Answers `201`, as the OAS gives |
| `DELETE /api/v2/resource-servers/{id}` | `204`. Deletes the API's client grants and takes its permissions from roles and users |

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

| Endpoint | Notes |
| - | - |
| `GET /api/v2/connections` | `strategy` and `name` filters, `fields` and `include_fields`, paging (100 per page by default) or checkpoints |
| `POST /api/v2/connections` | `name` (letters, digits, and inner dashes), `strategy: auth0`, `display_name`, `options`, `enabled_clients`, `realms`, `metadata` (10 string values), `is_domain_connection`, `show_as_button`. Names are unique (`409 A connection with the same name already exists`) |
| `GET /api/v2/connections/{id}` | `fields` and `include_fields` |
| `PATCH /api/v2/connections/{id}` | `options` replaces the whole object, so settings left out take their defaults; a missing `display_name` goes back to the name, as the OAS describes |
| `DELETE /api/v2/connections/{id}` | `202`, or `204` when it does not exist. Deletes the connection's users and clears `default_directory` if it named the connection |
| `GET /api/v2/connections/{id}/clients` | `{ clients: [{ client_id }], next }`, with `take` up to 1000 |
| `PATCH /api/v2/connections/{id}/clients` | `[{ client_id, status }]`, up to 50 changes, turning the connection on or off for each client. `204` |

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

| Endpoint | Notes |
| - | - |
| `GET /api/v2/users/{id}/sessions` | The user's live sessions |
| `DELETE /api/v2/users/{id}/sessions` | Ends them all. `202` |
| `GET`, `PATCH`, `DELETE /api/v2/sessions/{id}` | Updates take `session_metadata` (25 string values), which replaces the old; `null` or `{}` removes it. Delete answers `202` |
| `POST /api/v2/sessions/{id}/revoke` | Ends the session and revokes the refresh tokens issued in it. Needs both `delete:sessions` and `delete:refresh_tokens` |

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

| Endpoint | Notes |
| - | - |
| `GET /api/v2/users/{id}/refresh-tokens` | The user's live refresh tokens |
| `DELETE /api/v2/users/{id}/refresh-tokens` | Revokes them all. `202` |
| `GET /api/v2/refresh-tokens` | `user_id` (required), `client_id`, `fields` and `include_fields`, checkpoints |
| `POST /api/v2/refresh-tokens/revoke` | `ids` (up to 100), or `user_id`, optionally with `client_id`, and then optionally `audience`. `202` |
| `GET`, `PATCH`, `DELETE /api/v2/refresh-tokens/{id}` | Updates take `refresh_token_metadata` (25 string values), replaced whole. Delete answers `202` |

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

```json theme={null}
{
  "auth0": {
    "domain": "baseline.us.auth0.com",
    "friendly_name": "Acme",
    "default_directory": "Username-Password-Authentication",
    "default_audience": "https://api.example.com",
    "allowed_logout_urls": ["http://localhost:3000"],
    "revoke_refresh_token_grant": false,
    "connections": [
      {
        "name": "staff",
        "display_name": "Staff",
        "password_policy": "excellent",
        "requires_username": true,
        "disable_signup": false,
        "enabled_clients": ["baseline-web-app"]
      }
    ],
    "users": [
      {
        "email": "ada@example.com",
        "password": "Passw0rd!",
        "name": "Ada Lovelace",
        "given_name": "Ada",
        "family_name": "Lovelace",
        "user_metadata": {
          "plan": "pro"
        },
        "roles": ["reader"],
        "permissions": [
          {
            "api": "https://api.example.com",
            "scopes": ["write:items"]
          }
        ]
      },
      {
        "email": "sam@example.com",
        "password": "Passw0rd!",
        "connection": "staff",
        "username": "sam",
        "blocked": false
      }
    ],
    "apis": [
      {
        "identifier": "https://api.example.com",
        "name": "Example API",
        "scopes": [
          "read:items",
          {
            "value": "write:items",
            "description": "Write items"
          }
        ],
        "allow_offline_access": true,
        "token_lifetime": 86400,
        "token_lifetime_for_web": 7200,
        "enforce_policies": true,
        "token_dialect": "access_token_authz",
        "signing_alg": "RS256"
      }
    ],
    "roles": [
      {
        "name": "reader",
        "description": "Reads items",
        "permissions": [
          {
            "api": "https://api.example.com",
            "scopes": ["read:items"]
          }
        ]
      }
    ],
    "clients": [
      {
        "client_id": "baseline-web-app",
        "client_secret": "baseline-web-secret",
        "name": "Web app",
        "app_type": "regular_web",
        "callbacks": ["http://localhost:3000/callback"],
        "allowed_logout_urls": ["http://localhost:3000"],
        "grant_types": ["authorization_code", "refresh_token", "password"],
        "id_token_lifetime": 36000
      },
      {
        "client_id": "baseline-spa",
        "name": "Single page app",
        "app_type": "spa",
        "callbacks": ["http://localhost:5173/callback"],
        "refresh_token": {
          "rotation_type": "rotating",
          "leeway": 0,
          "idle_token_lifetime": 1296000
        }
      },
      {
        "client_id": "baseline-m2m",
        "client_secret": "baseline-m2m-secret",
        "name": "Backend service",
        "app_type": "non_interactive"
      }
    ],
    "client_grants": [
      {
        "client_id": "baseline-m2m",
        "audience": "https://api.example.com",
        "scope": ["read:items"]
      },
      {
        "client_id": "baseline-m2m",
        "audience": "https://baseline.us.auth0.com/api/v2/",
        "scope": [
          "read:users",
          "update:users",
          "read:roles",
          "read:clients",
          "read:resource_servers",
          "read:connections"
        ]
      }
    ]
  }
}
```

* `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

| | |
| - | - |
| Routes | 72 supported or partial, 0 planned |
| Route gate | fail |
| Spec gate | no public spec |
| SDKs tested | `auth0@7.5.0`, `jose@6.2.12`, `openid-client@6.8.8` |
| Dual mode | not yet |

See [CONFORMANCE.md](https://github.com/deploybaseline/cli/blob/main/CONFORMANCE.md) for every provider.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.