---
title: "Authentication"
---
By default Phoenix deploys with authentication disabled as you may be just trying Phoenix for the very first time or have Phoenix deployed in a VPC. However you might want to further protect access to your data via authentication. Below are the steps.


<Info>
Authentication will stop collecting traces and block all API access until API keys are created. For that reason we recommend scheduling some downtime if you have already deployed phoenix.
</Info>

<Frame>
  <iframe src="https://www.youtube.com/embed/e9I2voaGuQE" allowFullScreen style={{ width: '100%', height: '420px' }} allow="encrypted-media *;"></iframe>
</Frame>

## Setup

To enable authentication on your Phoenix, you will have to set two environment variables:

<table data-full-width="false">
  <thead>
    <tr>
      <th width="15%">Variable</th>
      <th width="70%">Description</th>
      <th width="15%">Example Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td width="15%"><strong>PHOENIX_ENABLE_AUTH</strong></td>
      <td width="70%">Set to <code>True</code> to enable authentication on your platform</td>
      <td width="15%"><strong>True</strong> or <strong>False</strong></td>
    </tr>
    <tr>
      <td width="15%"><strong>PHOENIX_SECRET</strong></td>
      <td width="70%">A long string value that is used to sign JWTs for your deployment. It should be a good mix of characters and numbers and should be kept in a secret store of some kind.</td>
      <td width="15%"><code>3413f9a7735bb780c6b8e4db7d946a492b64d26112a955cdea6a797f4c833593</code></td>
    </tr>
  </tbody>
</table>

The following environment variables are optional but recommended:

<table data-header-hidden data-full-width="false"><thead><tr><th width="300">Variable</th><th>Description</th></tr></thead><tbody><tr><td><strong>PHOENIX_USE_SECURE_COOKIES</strong></td><td>If set to <strong>True</strong>, access and refresh tokens will be stored in secure cookies. Defaults to <strong>False</strong>.</td></tr><tr><td><strong>PHOENIX_CSRF_TRUSTED_ORIGINS</strong></td><td>A comma-separated list of origins allowed to bypass Cross-Site Request Forgery (CSRF) protection. This setting is recommended when configuring OAuth2 clients or sending password reset emails. If this variable is left unspecified or contains no origins, CSRF protection will not be enabled. In such cases, when a request includes <code>origin</code> or <code>referer</code> headers, those values will not be validated.</td></tr></tbody></table>

Deploy Phoenix with the above environment variables set. You will know that you have setup authentication correctly if the UI navigates to to a login screen.

By default Phoenix will create an admin user account. To get started:

1. Log in as the admin user. The email should be **admin@localhost** and the password will be **admin**
2. Set a new password for admin. You will be prompted to set a new password. Use a sufficiently complex password and save it in a safe place.
3. Go to the settings page on the left nav and create your first system API key. This API key can be used to log traces, use the Phoenix client, and programmatically hit Phoenix's APIs. Store the system API key in a safe place.
4. In your application code, make sure to set the proper authentication headers with the system API key. Phoenix respects headers in the form of [bearer auth](https://swagger.io/docs/specification/authentication/bearer-authentication/), meaning that you should set the header in the form **Authorization: Bearer \<token\>.** Note that if you are using the Phoenix Client or Phoenix Otel, you simply need to set the **PHOENIX_API_KEY** environment variable.

Re-deploy your application with the API key created above and you will see traces stream in as before.

<Warning>
Initial admin password: You can set the initial password via the `PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD` environment variable. It is only read on first startup when the default admin account is created; subsequent changes have no effect if the account already exists. If you need to change the admin password later, do so from the UI or via an admin password reset.

Docker example:

```bash
docker run \
  -e PHOENIX_ENABLE_AUTH=true \
  -e PHOENIX_SECRET=change-me-32chars-min1digit1lower \
  -e PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD='strong-admin-password' \
  -p 6006:6006 arizephoenix/phoenix:latest
```

Helm users can set `auth.defaultAdminPassword` or provide the secret key `PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD` in the chart's Secret.
</Warning>

## Password Policy

By default, Phoenix requires passwords to be a minimum of 4 characters long. For deployments that need stricter security, you can enable a strong password policy by setting the `PHOENIX_ENABLE_STRONG_PASSWORD_POLICY` environment variable to `True`.

When the strong password policy is enabled, passwords must meet all of the following requirements:

- At least 12 characters long
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character (`!@#$%^&*()_+`)

```bash
export PHOENIX_ENABLE_STRONG_PASSWORD_POLICY=True
```

## Brute Force Login Protection

Phoenix includes built-in brute force login protection that temporarily locks accounts after repeated failed login attempts. By default, after `5` failed attempts, the account is locked out for 5 minutes.

* **PHOENIX\_BRUTE\_FORCE\_LOGIN\_PROTECTION\_MAX\_ATTEMPTS:** Maximum number of failed login attempts before temporary lockout. Defaults to `5`.
* **PHOENIX\_DISABLE\_BRUTE\_FORCE\_LOGIN\_PROTECTION:** Set to `true` to disable brute force login protection entirely. Defaults to `false`.

```bash
# Increase max attempts before lockout
export PHOENIX_BRUTE_FORCE_LOGIN_PROTECTION_MAX_ATTEMPTS=10
```

## User Management

Users can be added and removed from a Phoenix instance with authentication enabled. Users have one of three roles: `admin`, `member`, or `viewer`. See permissions below to learn more about the permissions for each role.

Only admins can manage phoenix users. They can add, delete, and reset the passwords of other users. To manage users go to the `/settings` page.

## Permissions

This section outlines the specific actions that users can perform based on their assigned roles within the system: **Admin**, **Member**, and **Viewer**. The permission matrix is divided into two main categories:

* Mutations: Operations that allow users to create, update, or delete data within the system.
* Queries: Operations that enable users to retrieve or view data from the system.

### Mutations

Mutations are operations that enable users to create, update, or delete data within the system. This permission matrix ensures that only authorized roles can execute sensitive actions, such as managing users and API keys, while allowing members to perform essential account-related updates like changing their own passwords and usernames. Viewers have read-only access and cannot perform mutations.

<Info>
Neither an **Admin**, **Member**, nor **Viewer** is permitted to change email addresses.
</Info>

<table><thead><tr><th>Action</th><th width="100" align="center">Admin</th><th width="100" align="center">Member</th><th width="100" align="center">Viewer</th></tr></thead><tbody><tr><td>Create User</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Delete User</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Change Own Password</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td></tr><tr><td>Change Other's Password</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Change Own Username</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td></tr><tr><td>Change Other's Username</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Create System API Keys</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Delete System API Keys</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Create Own User API Keys</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td></tr><tr><td>Delete Own User API Keys</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td></tr><tr><td>Delete Other's User API Keys</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Create/Update Secrets</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Delete Secrets</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr></tbody></table>

### Queries

Queries are operations that allow users to retrieve and view data from the system.

<Info>
This table only shows actions that a **Member** or **Viewer** is not permitted to do. Actions without restrictions (such as viewing traces, projects, datasets, etc.) are omitted.
</Info>

<table><thead><tr><th>Action</th><th width="100" align="center">Admin</th><th width="100" align="center">Member</th><th width="100" align="center">Viewer</th></tr></thead><tbody><tr><td>List All System API Keys</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>List All User API Keys</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>List All Users</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>Fetch Other User's Info, e.g. emails</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr><tr><td>View Secret Values</td><td align="center">✅ Yes</td><td align="center">No</td><td align="center">No</td></tr></tbody></table>

### REST API Permissions (v1/ endpoints)

For programmatic access via REST API endpoints (paths beginning with `/v1/`), permissions are determined by both the user's role and the HTTP method used:

<table><thead><tr><th>Endpoint Category</th><th width="120" align="center">Admin</th><th width="120" align="center">Member</th><th width="120" align="center">Viewer</th></tr></thead><tbody><tr><td><strong>GET requests</strong> (read operations)<br />Projects, datasets, experiments, prompts, spans, traces, annotations, annotation configs</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td></tr><tr><td><strong>POST/PUT/DELETE requests</strong> (write operations)<br />Creating, updating, or deleting resources</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td><td align="center">❌ No<br />(403 Forbidden)</td></tr><tr><td><strong>Personal API-key self-service</strong><br /><code>GET /v1/user/api_keys</code><br /><code>POST /v1/user/api_keys</code><br /><code>DELETE /v1/user/api_keys/{id}</code></td><td align="center">✅ Yes</td><td align="center">✅ Yes</td><td align="center">✅ Yes</td></tr><tr><td><strong>API-key administration</strong><br /><code>GET /v1/users/api_keys</code><br /><code>GET/POST/DELETE /v1/system/api_keys</code></td><td align="center">✅ Yes</td><td align="center">❌ No<br />(403 Forbidden)</td><td align="center">❌ No<br />(403 Forbidden)</td></tr><tr><td><strong>User management endpoints</strong><br /><code>GET /v1/users</code><br /><code>POST /v1/users</code><br /><code>DELETE /v1/users/{id}</code></td><td align="center">✅ Yes</td><td align="center">❌ No<br />(403 Forbidden)</td><td align="center">❌ No<br />(403 Forbidden)</td></tr><tr><td><strong>Project management endpoints</strong><br /><code>PUT /v1/projects/{id}</code><br /><code>DELETE /v1/projects/{id}</code></td><td align="center">✅ Yes</td><td align="center">❌ No<br />(403 Forbidden)</td><td align="center">❌ No<br />(403 Forbidden)</td></tr><tr><td><strong>Secrets management endpoints</strong><br /><code>PUT /v1/secrets</code></td><td align="center">✅ Yes</td><td align="center">❌ No<br />(403 Forbidden)</td><td align="center">❌ No<br />(403 Forbidden)</td></tr></tbody></table>

<Warning>
**Viewer role restrictions for REST API:**
* Viewers have **read-only access** to v1/ endpoints
* All GET requests are allowed (viewing projects, datasets, experiments, traces, spans, etc.)
* Write operations (POST, PUT, DELETE) return **403 Forbidden**, except that viewers may create and revoke their own user API keys
* User management and project CRUD operations are also blocked

**Examples of blocked operations for Viewers:**
* Creating datasets: `POST /v1/datasets/upload`
* Creating experiments: `POST /v1/datasets/{id}/experiments`
* Creating annotations: `POST /v1/span_annotations`
* Uploading traces: `POST /v1/traces`
* Deleting resources: `DELETE /v1/datasets/{id}`, `DELETE /v1/experiments/{id}`
</Warning>

## API Keys

There are two kinds of API keys in Phoenix: `system` and `user`.

### System Keys

System keys act on behalf of the system as a whole rather than any particular user. They can only be created by admins, are not meaningfully associated with the admin who creates them except for auditing purposes, and do not disappear if that admin is deleted. A system key would be the recommended kind of key to use in programmatic interactions with Phoenix that do not involve a user (e.g., automated flows querying our REST APIs).

Creating a system key requires an admin access-token session or `PHOENIX_ADMIN_SECRET`, on both REST and GraphQL. API keys cannot mint additional keys.

### User Keys

User API keys are associated with and act on behalf of the user to which they are issued. That user has the ability to view and delete their own user keys, and if the user is deleted, so are all of their associated user keys. A user might create their own user key into order to run an experiment in a notebook, for example.

Creating a user key requires an access-token session, on both REST and GraphQL; an existing API key cannot mint a replacement. Through Phoenix 18, GraphQL allowed API-key-authenticated issuance for backward compatibility; as of Phoenix 19 both surfaces enforce the same non-transitive policy. Workflows that used an API key for unattended key creation must instead provision keys from a logged-in session (for example, the Settings page or a script that authenticates with user credentials).

### Setting and Using API Keys with Environment Variables

Phoenix API keys can be set with the `PHOENIX_API_KEY` environment variable:

```bash
export PHOENIX_API_KEY=<SYSTEM-OR-USER-KEY>
```

If authentication is enabled on Phoenix, all interactions with the server need to include an `authorization` header. Phoenix will read the `PHOENIX_API_KEY` environment variable, and automatically include it as an `authorization` header. Interactions with Phoenix include:

* Using `phoenix.Client`
* Running experiments
* Sending OpenInference traces (more details below)

### Sending OpenInference traces

API Keys also need to be included on OpenInference traces sent to the Phoenix server. If you've set the `PHOENIX_API_KEY` environment variable, the `phoenix.otel` module will automatically include an `authorization` header with the API key:

```python
from phoenix.otel import register

tracer_provider = register()
```

Alternatively, you can explicitly set the `authorization` header on the exporter if using OpenTelemetry primitives directly.

```python
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk import trace as trace_sdk
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

endpoint = "http://127.0.0.1:6006/v1/traces"
tracer_provider = trace_sdk.TracerProvider()
exporter = OTLPSpanExporter(
    endpoint,
    headers={"authorization": "Bearer <SYSTEM-OR-USER-KEY>"},
)
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
```

<Info>
If setting `authorization` headers explicitly, ensure that the header field is **lowercased** to ensure compatibility with sending traces via gRPC
</Info>

## Agent Authentication Discovery

Phoenix serves two discovery endpoints so AI agents can learn how to authenticate against a deployed instance. Both are always available and require no configuration:

- `GET /auth.md` — a human- and agent-readable Markdown guide describing this deployment's auth model. When authentication is disabled it states that all endpoints are public; when enabled it directs the caller to create an API key under **Settings → API Keys** and send an `Authorization: Bearer <token>` header. It follows the [auth.md convention](https://workos.com/auth-md/docs/apps).
- `GET /.well-known/oauth-protected-resource` — [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) Protected Resource Metadata (JSON), advertising the resource, that bearer tokens are sent in the header, and a link to `/auth.md`.

When authentication is enabled, `401 Unauthorized` responses include a `WWW-Authenticate` header pointing at the Protected Resource Metadata endpoint, giving agents a stable entry point for discovering how to authenticate. When the [OAuth2 authorization server](#oauth2-authorization-server) is enabled, the metadata also advertises it, so OAuth-capable clients (such as MCP clients) can log in interactively; other clients are provisioned out of band with [API keys](#api-keys), and access is always governed by the role of the user the credential belongs to.

## OAuth2 Authorization Server

When authentication is enabled, Phoenix also acts as its own **OAuth2 authorization server** so that interactive clients — MCP clients like Claude Code and Cursor connecting to the [Remote MCP Server](/docs/phoenix/integrations/remote-mcp), and the Phoenix CLI — can log in through the browser instead of handling long-lived API keys. The flow is standard authorization code with PKCE: clients register via [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) dynamic client registration, discover endpoints via [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) server metadata, and a token is minted only after a logged-in user approves the consent page. Tokens are audience-bound to the resource they were minted for and carry the permissions of the approving user.

<Note>
Don't confuse this with [Configuring OAuth2 Identity Providers](#configuring-oauth2-identity-providers) below, where Phoenix is the *client* logging your users in against Google, Okta, etc. (`PHOENIX_OAUTH2_<IDP>_*` variables). Here Phoenix is the *server*, issuing its own tokens to MCP and CLI clients.
</Note>

Registration alone grants no authority: an authorization code is delivered only to the exact registered redirect URI, PKCE binds the exchange to the flow's initiator, and nothing is issued without a logged-in user's consent. The defaults are designed to work out of the box with MCP clients in the wild; the variables below let you tighten or disable the surface.

| Variable | Description |
|---|---|
| **PHOENIX\_ENABLE\_OAUTH2\_AUTHORIZATION\_SERVER** | Whether Phoenix serves the built-in authorization server. Defaults to `true`. When `false`, the `/oauth2/*` endpoints and the RFC 8414 discovery document respond 404, and the protected-resource metadata stops advertising an authorization server. Previously minted access tokens remain valid until they expire, but grants can no longer be refreshed. API keys and browser login are unaffected. Has no effect unless authentication is enabled. |
| **PHOENIX\_OAUTH2\_GRANT\_EXPIRY\_DAYS** | How long, in days, an OAuth2 grant lives. Refresh-token rotation never extends a token's life past its grant's expiry, so this is the hard ceiling on how long a consent lasts before the user must log in again. Defaults to `90`. |
| **PHOENIX\_OAUTH2\_DYNAMIC\_CLIENT\_REGISTRATION** | Which redirect URI mechanisms public clients may register. One of `enabled`, `local_only`, `disabled`. Defaults to `enabled` — MCP clients in the wild (e.g., Cursor) register HTTPS redirect URIs alongside loopback and private-use-scheme ones, and rejecting HTTPS breaks those clients out of the box. Set to `local_only` to accept only loopback and private-use-scheme redirects, or `disabled` to turn off dynamic registration entirely. |
| **PHOENIX\_OAUTH2\_ALLOWED\_REDIRECT\_HOSTS** | A comma-separated list of hosts allowed as HTTPS redirect targets for dynamic client registration, e.g. `PHOENIX_OAUTH2_ALLOWED_REDIRECT_HOSTS=cursor.com,example.com`. When unset, all HTTPS redirect hosts are accepted while dynamic client registration is `enabled`. Use this to keep browser-based clients working while pinning exactly which services may receive authorization codes. |
| **PHOENIX\_OAUTH2\_CONSENT\_ORIGIN\_CHECK** | Origin-header enforcement on consent-page decisions, a defense against cross-site consent forgery. One of `strict`, `off`. Defaults to `strict`. Only relax this if a reverse proxy strips the `Origin` header and consent approvals fail. |
| **PHOENIX\_OAUTH2\_DCR\_RATE\_LIMIT\_PER\_HOUR** | Maximum dynamic client registrations accepted per IP address each hour. Defaults to `10`. |

## Password Recovery

<Info>
The password recovery methods described in this section apply when recovering a locally authenticated user's password. In order recover a password for a user logged in via a third-party identity provider such as Google, you will have to consult the documentation of these identity providers
</Info>

### With SMTP (Simple Mail Transfer Protocol)

Using SMTP ensures that your password recovery emails are delivered reliably and securely. SMTP is the standard protocol for sending emails, making sure that you receive the reset link promptly in your inbox.

Below is an example configuration to enable SMTP for `sendgrid`.

```properties
export PHOENIX_SMTP_HOSTNAME=smtp.sendgrid.net
export PHOENIX_SMTP_USERNAME=apikey
export PHOENIX_SMTP_PASSWORD=XXXXXXXXXXXXXXXXX
```

### Without SMTP

If SMTP is not configured, you have a few options to recover your forgotten password:

* Contact an administrator and request that they reset your password. Admins can reset user passwords on the `settings` page.
* As a last resort, you can manually update the database tuple that contains your password salt and hash.

## Configuring OAuth2 Identity Providers

Phoenix supports login via third-party identity providers (IDPs), including:

* Google
* [AWS Cognito](https://aws.amazon.com/cognito/)
* [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id) (previously known as Azure Active Directory)
* IDPs that support [OpenID Connect](https://openid.net/developers/how-connect-works/) and a [well-known configuration endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest) at `GET /.well-known/openid-configuration`

<Info>
OAuth2 enables applications such as Phoenix to authorize access to resources via identity providers (IDPs) rather than storing and verifying user credentials locally. OpenID Connect is an extension of OAuth2 that additionally authenticates users by verifying identity and providing Phoenix with user information such as email address, username, etc. Phoenix integrates with OpenID Connect IDPs that have a "well-known configuration endpoint" at `GET /.well-known/openid-configuration`, which provides a standardized way to discover information about the IDP's endpoints and capabilities.
</Info>

Phoenix uses the OAuth2 authorization code flow for web applications, which requires setting a few environment variables in addition to `PHOENIX_ENABLE_AUTH` and `PHOENIX_SECRET`:

<table data-full-width="false">
  <thead>
    <tr>
      <th width="240">Environment Variable</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>PHOENIX\_OAUTH2\_\<IDP\>\_CLIENT\_ID</strong></td>
      <td>The client ID generated by the IDP when registering the application. (Required)</td>
    </tr>
    <tr>
      <td><strong>PHOENIX\_OAUTH2\_\<IDP\>\_CLIENT\_SECRET</strong></td>
      <td>The client secret generated by the IDP when registering the application. Required by default for confidential clients. Only optional when <code>TOKEN_ENDPOINT_AUTH_METHOD</code> is explicitly set to <code>none</code> (for public clients without client authentication).</td>
    </tr>
    <tr>
      <td><strong>PHOENIX\_OAUTH2\_\<IDP\>\_OIDC\_CONFIG\_URL</strong></td>
      <td>The URL to the OpenID Connect well-known configuration endpoint. Entering this URL in your browser will return a JSON object containing authorization server metadata. Must be HTTPS except for localhost. (Required)</td>
    </tr>
  </tbody>
</table>

### Optional OAuth2 Configuration

The following optional environment variables provide additional control over OAuth2 authentication behavior:

| Environment Variable | Description |
|---------------------|-------------|
| **PHOENIX\_OAUTH2\_\<IDP\>\_DISPLAY\_NAME** | A user-friendly name for the identity provider shown in the UI. If not set, Phoenix will generate a display name based on the IDP identifier. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_ALLOW\_SIGN\_UP** | Whether to allow new user registration via this OAuth2 provider. Defaults to `True`. When set to `False`, only existing users can sign in. The system will check if the user exists in the database by their email address. If the user does not exist or has a password set (local auth), they will be redirected to the login page with an error message. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_AUTO\_LOGIN** | Automatically redirect to this provider's login page, skipping the Phoenix login screen. Defaults to `False`. Useful for single sign-on deployments. Note: Only one provider should have `AUTO_LOGIN` enabled if you configure multiple IDPs. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_USE\_PKCE** | Enable PKCE (Proof Key for Code Exchange) with S256 code challenge method for enhanced security. PKCE protects the authorization code from interception and can be used with both public clients and confidential clients. This setting is orthogonal to client authentication—whether `CLIENT_SECRET` is required is determined solely by `TOKEN_ENDPOINT_AUTH_METHOD`, not by `USE_PKCE`. Defaults to `False`. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_TOKEN\_ENDPOINT\_AUTH\_METHOD** | OAuth2 token endpoint authentication method. This setting determines how the client authenticates with the token endpoint and whether `CLIENT_SECRET` is required. If not set, defaults to requiring `CLIENT_SECRET` (confidential client). Options:<br />• `client_secret_basic`: Send credentials in HTTP Basic Auth header (most common). `CLIENT_SECRET` is required. This is the assumed default behavior if not set.<br />• `client_secret_post`: Send credentials in POST body (required by some providers). `CLIENT_SECRET` is required.<br />• `none`: No client authentication (for public clients). `CLIENT_SECRET` is not required. Use this for public clients that cannot securely store a client secret, typically in combination with `PKCE`.<br /><br />Most providers work with the default behavior. Set this explicitly only if your provider requires a specific method or if you're configuring a public client. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_SCOPES** | Additional OAuth2 scopes to request (space-separated). These are added to the required baseline scopes `openid email profile`. For example, set to `offline_access groups` to request refresh tokens and group information. The baseline scopes are always included and cannot be removed. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_EMAIL\_ATTRIBUTE\_PATH** | JMESPath expression to extract email from the OIDC ID token or userinfo endpoint response. Defaults to `email` (standard OIDC claim). See [jmespath.org](https://jmespath.org) for full syntax.<br /><br />**When to use:** Some identity providers (notably Microsoft Entra ID/Azure AD) may not populate the `email` claim for users who don't have the `mail` attribute in their directory. In these cases, you can extract the User Principal Name (UPN) from `preferred_username` instead.<br /><br />Common patterns:<br />• `email` (default) - standard OIDC email claim<br />• `preferred_username` - Azure AD/Entra ID UPN (recommended when email is null)<br />• `upn` - Azure AD UPN (requires optional claim configuration in Azure Portal)<br />• `attributes.email` - nested email structure<br /><br />Values are automatically lowercased and trimmed before storage for consistent matching. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_GROUPS\_ATTRIBUTE\_PATH** | JMESPath expression to extract group/role claims from the OIDC ID token or userinfo endpoint response. The path navigates nested JSON structures to find group/role information. This claim is checked from both the ID token and userinfo endpoint (if available). The result is normalized to a list of strings for group matching. See [jmespath.org](https://jmespath.org) for full syntax.<br /><br />**⚠️ IMPORTANT:** Claim keys with special characters (colons, dots, slashes, hyphens, etc.) MUST be enclosed in double quotes.<br /><br />Common JMESPath patterns:<br />• Simple keys: groups - extracts top-level array<br />• Nested keys: resource_access.phoenix.roles - dot notation for nested objects<br />• Array projection: teams[*].name - extracts 'name' field from each object in array<br />• Array indexing: groups[0] - gets first element<br /><br />Common provider examples:<br />• Google Workspace: `groups`<br />• Azure AD/Entra ID: `roles` or `groups`<br />• Keycloak: `resource_access.phoenix.roles` (nested structure)<br />• AWS Cognito: `"cognito:groups"` (use quotes for colon)<br />• Okta: `groups`<br />• Auth0 (custom namespace): `"https://myapp.com/groups"` (use quotes for special chars)<br />• Custom objects: `teams[*].name` (extract field from array of objects)<br /><br />If not set, group-based access control is disabled for this provider. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_ALLOWED\_GROUPS** | Comma-separated list of group names that are permitted to sign in. Users must belong to at least one of these groups (extracted via `GROUPS_ATTRIBUTE_PATH`) to authenticate successfully. Works together with `GROUPS_ATTRIBUTE_PATH` to implement group-based access control. If not set, all authenticated users can sign in (subject to `ALLOW_SIGN_UP` restrictions).<br /><br />Example: `PHOENIX_OAUTH2_OKTA_ALLOWED_GROUPS="admin,developers,viewers"`<br /><br />Note: Both `GROUPS_ATTRIBUTE_PATH` and `ALLOWED_GROUPS` must be configured together. If one is set, the other is required. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_ROLE\_ATTRIBUTE\_PATH** | JMESPath expression to extract user role claim from the OIDC ID token or userinfo endpoint response. Similar to `GROUPS_ATTRIBUTE_PATH` but for extracting a single role value. See [jmespath.org](https://jmespath.org) for full syntax.<br /><br />**⚠️ IMPORTANT:** Claim keys with special characters MUST be enclosed in double quotes.<br />Examples: `"https://myapp.com/role"`, `"custom:role"`, `user.profile."app-role"`<br /><br />Common patterns:<br />• Simple key: `role` - extracts top-level string<br />• Nested key: `user.organization.role` - dot notation for nested objects<br />• Array element: `roles[0]` - gets first role from array<br />• Constant value: `'MEMBER'` - assigns a fixed role to all users from this IDP (notice the single quotes)<br />• Conditional logic: `contains(groups[*], 'admin') && 'ADMIN' \|\| 'VIEWER'` - compute role from group membership using logical operators (returns Phoenix role directly, no mapping needed)<br /><br />This claim is used with `ROLE_MAPPING` to automatically assign Phoenix roles (ADMIN, MEMBER, VIEWER) based on the user's role in your identity provider. The extracted role value is matched against keys in `ROLE_MAPPING` to determine the Phoenix role.<br /><br />**Advanced:** If the JMESPath expression returns a valid Phoenix role name (ADMIN, MEMBER, VIEWER) directly, `ROLE_MAPPING` is optional - the value will be used as-is after case-insensitive validation.<br /><br />**⚠️ Role Update Behavior:**<br />• When `ROLE_ATTRIBUTE_PATH` IS configured: User roles are synchronized from the IDP on EVERY login. This ensures Phoenix roles stay in sync with your IDP's role assignments.<br />• When `ROLE_ATTRIBUTE_PATH` is NOT configured: User roles are preserved as-is (backward compatibility). New users get VIEWER role (least privilege), existing users keep their current roles. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_ROLE\_MAPPING** | Maps identity provider role values to Phoenix roles. Format: `IdpRole1:PhoenixRole1,IdpRole2:PhoenixRole2`<br /><br />Phoenix roles (case-insensitive):<br />• **ADMIN**: Full system access, can manage users and settings<br />• **MEMBER**: Standard user access, can create and manage own resources<br />• **VIEWER**: Read-only access, cannot create or modify resources<br /><br />Example mappings:<br />•`PHOENIX_OAUTH2_OKTA_ROLE_MAPPING="Owner:ADMIN,Developer:MEMBER,Guest:VIEWER"`<br />•`PHOENIX_OAUTH2_KEYCLOAK_ROLE_MAPPING="admin:ADMIN,user:MEMBER"`<br /><br />**⚠️ Security:** The SYSTEM role cannot be assigned via OAuth2. Attempts to map to SYSTEM will be rejected.<br /><br />**Optional Behavior (no mapping required):**<br />If `ROLE_MAPPING` is not configured but `ROLE_ATTRIBUTE_PATH` is set, the system will use the IDP role value directly if it exactly matches "ADMIN", "MEMBER", or "VIEWER" (case-insensitive). This allows IDPs that already use Phoenix's role names to work without explicit mapping.<br /><br />IDP role keys are case-sensitive and must match exactly. Phoenix role values are case-insensitive but will be normalized to uppercase (ADMIN, MEMBER, VIEWER). If a user's IDP role is not in the mapping, behavior depends on `ROLE_ATTRIBUTE_STRICT`:<br />• strict=false (default): User gets VIEWER role (least privilege)<br />• strict=true: User is denied access<br /><br />Works together with `ROLE_ATTRIBUTE_PATH`. If `ROLE_ATTRIBUTE_PATH` is set but `ROLE_MAPPING` is not, the IDP role value is used directly if it matches a valid Phoenix role (ADMIN, MEMBER, VIEWER). If the IDP role doesn't match a valid Phoenix role, behavior depends on `ROLE_ATTRIBUTE_STRICT`. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_ROLE\_ATTRIBUTE\_STRICT** | Controls behavior when role cannot be determined from identity provider claims. Defaults to `false`.<br /><br />When `true`:<br />• Missing role claim → access denied<br />• Role not in ROLE_MAPPING → access denied<br />• Empty/invalid role value → access denied<br /><br />When `false` (default):<br />• Missing/unmapped/invalid role → user gets VIEWER role (least privilege, fail-safe)<br /><br />Note: this VIEWER fail-safe applies to new users and to existing users whose roles are synced. If `ROLE_RESYNC=false`, existing users keep their current role even when their IDP role claim is missing or invalid, so the fail-safe downgrade does not apply to them.<br /><br />Strict mode is recommended for high-security environments where all users must have explicitly assigned roles. Non-strict mode (default) is more forgiving and suitable for gradual rollout of role mapping.<br /><br />Example: `PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_STRICT=true`<br /><br />Note: This setting only applies when `ROLE_ATTRIBUTE_PATH` is configured. If `ROLE_ATTRIBUTE_PATH` is not set, this setting is ignored. |
| **PHOENIX\_OAUTH2\_\<IDP\>\_ROLE\_RESYNC** | Controls whether existing users' roles are re-synchronized from the IDP on every login. Defaults to `true`.<br /><br />When `true` (default): If `ROLE_ATTRIBUTE_PATH` is configured, an existing user's role is overwritten from IDP claims on every login (the IDP is the source of truth).<br /><br />When `false`: An existing user's role is **never** overwritten from IDP claims. New users are still created with their IDP-mapped role, so role mapping stays active for provisioning, but existing users keep their current Phoenix role on re-login.<br /><br />Set this to `false` when you want IDP-based role mapping to provision new users while still being able to promote or demote specific users from within Phoenix. Example: `PHOENIX_OAUTH2_OKTA_ROLE_RESYNC=false`<br /><br />**⚠️ Security:** With this disabled (`false`), Phoenix becomes the source of truth for existing users' roles. Revoking a user's role at the IDP (removing the role claim or the group that maps to it) will **not** automatically demote them in Phoenix — including the non-strict VIEWER fail-safe. You must deprovision or downgrade such users manually in the Phoenix admin UI.<br /><br />Note: This setting only applies when `ROLE_ATTRIBUTE_PATH` is configured. Setting it to `false` without `ROLE_ATTRIBUTE_PATH` is rejected at startup, since there is no role sync to disable. |


<Warning>
**Group-based access control requirements:**
* If you set `ALLOWED_GROUPS`, you must also set `GROUPS_ATTRIBUTE_PATH` to extract groups from the ID token.
* If you set `GROUPS_ATTRIBUTE_PATH`, you must also set `ALLOWED_GROUPS` to specify which groups are allowed.
* Group-based access control is evaluated per-provider: if a user authenticates via an IDP with `ALLOWED_GROUPS` configured, they must belong to one of those groups to sign in.

**Role mapping configuration:**
* `ROLE_ATTRIBUTE_PATH` and `ROLE_MAPPING` work together to automatically assign Phoenix roles based on IDP roles.
* If `ROLE_ATTRIBUTE_PATH` is configured, user roles are synchronized from the IDP on every login — unless `ROLE_RESYNC` is disabled (`false`), in which case existing users keep their current Phoenix roles (new users are still provisioned with their mapped role).
* If `ROLE_ATTRIBUTE_PATH` is not configured, new OAuth2 users get the VIEWER role by default and existing users keep their current roles.
* Set `ROLE_RESYNC=false` to keep role mapping active for provisioning while letting admins manage individual users' roles from the Phoenix UI without being overwritten on the next login.
* Groups control **access** (who can sign in), while roles control **permissions** (what users can do).
</Warning>

### Where Phoenix Reads Group and Role Claims

`GROUPS_ATTRIBUTE_PATH` and `ROLE_ATTRIBUTE_PATH` are evaluated against the claims Phoenix receives at login as an OpenID Connect client: the **verified ID token** and the **UserInfo response**. Phoenix checks the ID token first and, when a configured claim is missing there, fetches the UserInfo endpoint and checks the response.

<Warning>
**The group/role claim must be present on the ID token or the UserInfo response.** This is an identity-provider setting, not a Phoenix one. If your IdP places authorization data (groups, roles) only on the **OAuth2 access token**, Phoenix will not see it. What happens then depends on your configuration: when group-based access control (`GROUPS_ATTRIBUTE_PATH` + `ALLOWED_GROUPS`) is enabled, a missing group claim **denies sign-in**; for role mapping, a missing role claim falls back to `VIEWER` in non-strict mode (the default) but **denies sign-in** when `ROLE_ATTRIBUTE_STRICT=true`.

Phoenix uses the access token solely as a bearer credential to call the UserInfo endpoint; it does not decode the access token to read claims. Per [RFC 9068 §6](https://datatracker.ietf.org/doc/html/rfc9068#section-6), OAuth2 clients are directed to treat access tokens as opaque, so the supported claim surfaces for a sign-in application are the ID token and UserInfo.
</Warning>

**Keeping authorization claims off the ID token.** If your security policy is to keep authorization data out of the ID token, the **UserInfo response is a fully supported target**. Configure your IdP to return the group/role claim from UserInfo (rather than embedding it in every ID token), request the appropriate scope, and point `GROUPS_ATTRIBUTE_PATH` / `ROLE_ATTRIBUTE_PATH` at the claim. When a configured claim is absent from the ID token, Phoenix fetches UserInfo with the access token during login, so a claim exposed only through UserInfo is still picked up — with no access-token decoding involved.

**Provider claim configuration.** Exposing the claim is done in your IdP's console. Common starting points (consult each provider's current docs for exact steps):

* **Okta** — request the `groups` scope from Phoenix (`PHOENIX_OAUTH2_OKTA_SCOPES=groups`) and add a `groups` claim. On the **org authorization server**, configure the app's Groups claim and filter; on a **custom authorization server**, add the claim under **Security → API → Authorization Servers → (your server) → Claims**. To return the claim through UserInfo (instead of forcing it into every ID token), choose the **Userinfo/id_token request** option. See Okta's [groups claim guide](https://developer.okta.com/docs/guides/customize-tokens-groups-claim/main/) and [custom claims docs](https://help.okta.com/en-us/Content/Topics/Security/api-config-claims.htm).
* **Auth0** — add a namespaced custom claim to the ID token from a Post-Login Action with `api.idToken.setCustomClaim(...)`.
* **Microsoft Entra ID (Azure AD)** — configure the optional `groups` claim (or app-role `roles`) in the app registration's **Token configuration**, with handling for group overage on large group lists.
* **Keycloak** — add a Group Membership or role mapper to the client or client scope with **Add to ID token** and/or **Add to userinfo** enabled.

<Tip>
**Troubleshooting: unexpected `VIEWER` role or denied sign-in.** Both symptoms usually mean the configured claim isn't reaching the ID token or UserInfo response — a missing role claim falls back to `VIEWER` in non-strict mode, while a missing group claim denies sign-in when group access control is enabled (as does a missing role claim under `ROLE_ATTRIBUTE_STRICT=true`). Check that (1) Phoenix requests the scope that releases the claim (e.g. `groups` for Okta), (2) the claim is configured for the ID token or UserInfo on the IdP side, and (3) your JMESPath path matches the claim's actual key and shape. A claim that exists only on the access token will not be visible to Phoenix.
</Tip>

### Multiple Identity Providers

You can configure multiple IDPs simultaneously by setting environment variables for each provider with different IDP identifiers. Users will see all configured providers as login options on the Phoenix login page. Each IDP is configured independently with its own set of variables.

Example with both Google and Okta:

```bash
# Google OAuth
export PHOENIX_OAUTH2_GOOGLE_CLIENT_ID=google_client_id
export PHOENIX_OAUTH2_GOOGLE_CLIENT_SECRET=google_secret
export PHOENIX_OAUTH2_GOOGLE_OIDC_CONFIG_URL=https://accounts.google.com/.well-known/openid-configuration

# Internal Okta with group restrictions
export PHOENIX_OAUTH2_OKTA_CLIENT_ID=okta_client_id
export PHOENIX_OAUTH2_OKTA_CLIENT_SECRET=okta_secret
export PHOENIX_OAUTH2_OKTA_OIDC_CONFIG_URL=https://your-domain.okta.com/.well-known/openid-configuration
export PHOENIX_OAUTH2_OKTA_GROUPS_ATTRIBUTE_PATH=groups
export PHOENIX_OAUTH2_OKTA_ALLOWED_GROUPS="engineering,platform-team"
```

### Common OAuth2 Configuration Examples

**Public client with PKCE (no client secret):**

<Info>
PKCE support requires Phoenix 12.4.0 or later.
</Info>

```bash
export PHOENIX_OAUTH2_MOBILE_CLIENT_ID=mobile_app_id
export PHOENIX_OAUTH2_MOBILE_OIDC_CONFIG_URL=https://auth.example.com/.well-known/openid-configuration
export PHOENIX_OAUTH2_MOBILE_TOKEN_ENDPOINT_AUTH_METHOD=none
export PHOENIX_OAUTH2_MOBILE_USE_PKCE=true
```

**With nested group path (Keycloak):**

```bash
export PHOENIX_OAUTH2_KEYCLOAK_GROUPS_ATTRIBUTE_PATH=resource_access.phoenix.roles
export PHOENIX_OAUTH2_KEYCLOAK_ALLOWED_GROUPS="admin,developer"
```

**With special characters in path (AWS Cognito - quotes REQUIRED):**

```bash
export PHOENIX_OAUTH2_COGNITO_GROUPS_ATTRIBUTE_PATH='"cognito:groups"'
export PHOENIX_OAUTH2_COGNITO_ALLOWED_GROUPS="Administrators,PowerUsers"
```

**With namespaced claims (Auth0 - quotes REQUIRED):**

```bash
export PHOENIX_OAUTH2_AUTH0_GROUPS_ATTRIBUTE_PATH='"https://myapp.com/groups"'
export PHOENIX_OAUTH2_AUTH0_ALLOWED_GROUPS="admin,users"
```

**With array projection (extract names from objects):**

```bash
export PHOENIX_OAUTH2_CUSTOM_GROUPS_ATTRIBUTE_PATH="teams[*].name"
export PHOENIX_OAUTH2_CUSTOM_ALLOWED_GROUPS="engineering,operations"
```

**Single sign-on with auto-login:**

```bash
export PHOENIX_OAUTH2_COMPANY_DISPLAY_NAME="Company SSO"
export PHOENIX_OAUTH2_COMPANY_AUTO_LOGIN=true
export PHOENIX_OAUTH2_COMPANY_ALLOW_SIGN_UP=false
```

**With custom email attribute path (Azure AD/Entra ID without email claim):**

```bash
# Use preferred_username (UPN) when email claim is null
export PHOENIX_OAUTH2_AZURE_AD_EMAIL_ATTRIBUTE_PATH=preferred_username
```

<Info>
This is useful when Azure AD users don't have the `mail` attribute populated in their directory. The `preferred_username` claim contains the User Principal Name (UPN), which is always in email format (e.g., `user@company.onmicrosoft.com`).
</Info>

**With role mapping (simple):**

```bash
export PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_PATH=role
export PHOENIX_OAUTH2_OKTA_ROLE_MAPPING="Owner:ADMIN,Developer:MEMBER,Viewer:VIEWER"
```

**With role mapping (nested path for Keycloak):**

```bash
export PHOENIX_OAUTH2_KEYCLOAK_ROLE_ATTRIBUTE_PATH=resource_access.phoenix.role
export PHOENIX_OAUTH2_KEYCLOAK_ROLE_MAPPING="admin:ADMIN,user:MEMBER"
```

**With role mapping in strict mode (deny unmapped roles):**

```bash
export PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_PATH=role
export PHOENIX_OAUTH2_OKTA_ROLE_MAPPING="Owner:ADMIN,Developer:MEMBER"
export PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_STRICT=true
```

**With conditional logic to compute role from groups (no mapping needed):**

```bash
export PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_PATH="contains(groups[*], 'admin') && 'ADMIN' || contains(groups[*], 'editor') && 'MEMBER' || 'VIEWER'"
```

**With both groups and roles (groups control access, roles control permissions):**

```bash
export PHOENIX_OAUTH2_OKTA_GROUPS_ATTRIBUTE_PATH=groups
export PHOENIX_OAUTH2_OKTA_ALLOWED_GROUPS="engineering,platform-team"
export PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_PATH=role
export PHOENIX_OAUTH2_OKTA_ROLE_MAPPING="Owner:ADMIN,Developer:MEMBER,Guest:VIEWER"
```

**With a fixed role for all users from an IDP**
```bash
export PHOENIX_OAUTH2_OKTA_GROUPS_ATTRIBUTE_PATH=groups
export PHOENIX_OAUTH2_OKTA_ALLOWED_GROUPS="engineering,platform-team"
export PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_PATH="'ADMIN'"
```

**With role mapping for provisioning, but admin-managed roles thereafter**
```bash
# New users are provisioned from their Okta group, but a role set in the Phoenix
# UI is never overwritten on subsequent logins.
export PHOENIX_OAUTH2_OKTA_ROLE_ATTRIBUTE_PATH=role
export PHOENIX_OAUTH2_OKTA_ROLE_MAPPING="Owner:ADMIN,Developer:MEMBER,Guest:VIEWER"
export PHOENIX_OAUTH2_OKTA_ROLE_RESYNC=false
```

<Info>
**Default role behavior:**
* If `ROLE_ATTRIBUTE_PATH` is **not configured**: New OAuth2 users are initially added as VIEWER (least privilege). Their role can be changed after their first login by a Phoenix admin. Existing users keep their current roles.
* If `ROLE_ATTRIBUTE_PATH` **is configured**: User roles are automatically synchronized from the IDP on every login based on the role mapping configuration, unless `ROLE_RESYNC=false`, which preserves existing users' current Phoenix roles while still provisioning new users from the IDP.
</Info>

Detailed instructions for common IDPs are provided below.

### Google

1. In Google Cloud Console, select a GCP project in which to register your Phoenix OAuth2 app.
2. Select **APIs and Services**.
3. In the **Credentials** page, click on **Create Credentials** and select **OAuth Client ID**.
4. From the **Application type** dropdown, select **Web application**.
5. Enter a name for your Phoenix app, which will be displayed to users when signing in.
6. Under **Authorized JavaScript origins**, click **Add URI** and enter the origin URL where you will access Phoenix in the browser.
7. Under **Authorized redirect URIs**, click **Add URI**. Take the URL from the previous step and append the slug `/oauth2/google/tokens`. Alternatively, if you have configured a root path via the `PHOENIX_HOST_ROOT_PATH` environment variable, append a slug of the form `/<root-path>/oauth2/google/tokens`. Enter the resulting URL.
8. Copy your client ID and client secret.
9. Deploy Phoenix with the three environment variables described above, substituting `GOOGLE` for `<IDP>`. The well-known configuration endpoint is `https://accounts.google.com/.well-known/openid-configuration`.

### AWS Cognito

1. In the AWS Management Console, navigate to the **Cognito** page.
2. From the **User Pools** page, select **Create User Pool**.
3. Under **Required attributes**, in the **Additional required attributes** dropdown, select **email** (you can optionally require **name** and **picture** to ensure user profiles have this information in Phoenix).
4. In the **Initial app client** section:
   1. Under **App type**, select **Confidential client**.
   2. Under **App client name**, enter a name for your Phoenix app.
   3. Under **Client secret**, ensure **Generate a client secret** is selected.
5. Create your user pool and navigate to the page for the newly created user pool by clicking on its name.
6. Add at least one user to your user pool in the **Users** section.
7. Copy and save your user pool ID from the top of the page. The ID should be of the form `<region>_<hash>`, e.g., `us-east-2_x4FTon498`.
8. Under **App Integration > Domain**, create a domain to contain the sign-in page and OAuth2 endpoints.
9. Under **App Integration > App client list > App clients and analytics**, select your newly created client.
10. Copy and save your client ID and client secret.
11. Under **Hosted UI**, click **Edit**. On the **Edit Hosted UI** page:
    1. Add an **Allowed callback URL** of the form `<origin-url>/oauth2/aws_cognito/tokens`, where `<origin-url>` is the URL where you will access Phoenix in the browser. Alternatively, if you have configured a root path via the `PHOENIX_HOST_ROOT_PATH` environment variable, your callback URL will have the form `<origin-url>/<root-path>/oauth2/aws_cognito/tokens`.
    2. In the **Identity Providers** dropdown, select **Cognito user pool**.
    3. Under **OAuth 2.0 grant types**, select **Authorization code grant**.
    4. Under **OpenID Connect scopes**, select **OpenID**, **Email**, and **Profile**.
    5. Save your changes.
12. The well-known configuration endpoint is of the form `https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/openid-configuration`, where the user pool ID was copied in a previous step and the region is the first part of the user pool ID preceding the underscore. Test this URL in your browser to ensure it is correct before proceeding to the next step.
13. Deploy Phoenix using the three environment variables described above, substituting `AWS_COGNITO` for `<IDP>`.

### Microsoft Entra ID

1. From the Azure portal, navigate to **Microsoft Entra ID**.
2. Select **Add > App Registration**.
3. On the **Register an Application** page:
   1. Enter a name for your application.
   2. Under **Redirect URI**, in the **Select a platform** dropdown, select **Web** and a redirect URI of the form `<origin-url>/oauth2/microsoft_entra_id/tokens`, where `<origin-url>` is the URL where you will access Phoenix in the browser. Alternatively, if you have configured a root path via the `PHOENIX_HOST_ROOT_PATH` environment variable, your redirect URI will have the form `<origin-url>/<root-path>/oauth2/microsoft_entra_id/tokens`.
4. Copy and save the **Application (client) ID**.
5. Under **Endpoints**, copy and save the well-known configuration endpoint under **OpenID Connect metadata document**.
6. Under **Client credentials**, click **Add a certificate or secret**. Create a client secret and copy and save its value.
7. Deploy Phoenix using the three environment variables described above, substituting `MICROSOFT_ENTRA_ID` for `<IDP>`.

<Tip>
**Missing email claim?** If your users see "Missing or invalid 'email' claim" errors, their directory entries may not have the `mail` attribute populated. Set `PHOENIX_OAUTH2_MICROSOFT_ENTRA_ID_EMAIL_ATTRIBUTE_PATH=preferred_username` to use the User Principal Name (UPN) instead, which is always available and in email format.
</Tip>

### Keycloak

1. From the Keycloak Console create a **new Realm** or skip this part if you want to reuse a existing Realm
2. Select **Clients**.
3. Click on **new Client**
   1. Enter the **Client ID** phoenix
   2. Enter the **Name** Phoenix Client
   3. Enter below **Root URL** the root url of your phoenix instance, like `https://example.com/subpath/subpath`
   4. Enter below **Home URL** the home url of your phoenix instance, like `/subpath/subpath`
   5. Enter below **Valid redirect URIs** a redirect url to your phoenix instance, like `https://example.com/subpath/subpath/*`
   6. Enter below **Valid post logout redirect URIs** +
   7. Enter below **Web origins** your url, like `https://example.com`
   8. Enter below **Admin URL** your admin url, like `https://example.com/subpath/subpath/`
   9. Enable **Client authentication**
   10. Ensure that only **Standard flow** and **Direct access grants** is enabled
   11. Hit the **Save button**
4. Go to the Client **phoenix** and to the tab credentials and copy the **client-secret**
5. Deploy Phoenix using the three environment variables described above, substituting `KEYCLOAK` for `<IDP>`:

```bash
PHOENIX_OAUTH2_KEYCLOAK_CLIENT_ID=""
PHOENIX_OAUTH2_KEYCLOAK_OIDC_CONFIG_URL="https://<your-keycloak-domain>/realms/<your-realm>/.well-known/openid-configuration"
PHOENIX_OAUTH2_KEYCLOAK_CLIENT_SECRET=""
```

### Other Identity Providers

Phoenix can integrate with any OAuth2 IDP that supports OpenID Connect and has a well-known configuration endpoint. Detailed instructions will vary by IDP, but the general steps remain the same:

1. Register a Phoenix client application with your IDP. If prompted to select an application type, select **traditional web application** or a similarly named application type that allows you to generate a client secret in addition to a client ID.
2. Find the well-known configuration endpoint for your IDP.
3. Deploy Phoenix with the environment variables described above, substituting `<IDP>` with your IDP name, e.g., `AUTH0`. If you have configured a root path via the `PHOENIX_HOST_ROOT_PATH` environment variable, ensure that the root path is included in the path of your callback URL.
4. Use the optional configuration variables documented above to customize behavior such as display names, sign-up policies, group-based access control, and more.

## Configuring LDAP Authentication

Phoenix supports authentication against LDAP directories, including:

* Microsoft Active Directory
* OpenLDAP
* 389 Directory Server
* Any LDAP v3 compliant directory

LDAP authentication allows users to log in with their corporate directory credentials, enabling centralized user management and integration with existing identity infrastructure.

<Info>
LDAP (Lightweight Directory Access Protocol) is the industry-standard protocol for accessing directory services. Phoenix uses LDAP to authenticate users against your corporate directory and optionally map directory groups to Phoenix roles.
</Info>

<Info>
LDAP support requires Phoenix 12.20.0 or later.
</Info>

### Required LDAP Configuration

To enable LDAP authentication, set the following environment variables in addition to `PHOENIX_ENABLE_AUTH` and `PHOENIX_SECRET`:

<table data-full-width="false">
  <thead>
    <tr>
      <th width="280">Environment Variable</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>PHOENIX_LDAP_HOST</strong></td>
      <td>LDAP server hostname or IP address. Comma-separated for multiple servers with automatic failover. (Required)<br /><br />Examples:<br />• Single server: <code>ldap.corp.example.com</code><br />• Multiple servers: <code>dc1.corp.com,dc2.corp.com,dc3.corp.com</code></td>
    </tr>
    <tr>
      <td><strong>PHOENIX_LDAP_USER_SEARCH_BASE_DNS</strong></td>
      <td>JSON array of base DNs (Distinguished Names) for user searches. Phoenix searches each base DN in order until a user is found. (Required)<br /><br />Examples:<br />• Active Directory: <code>["OU=Users,DC=corp,DC=example,DC=com"]</code><br />• OpenLDAP: <code>["ou=people,dc=example,dc=com"]</code></td>
    </tr>
    <tr>
      <td><strong>PHOENIX_LDAP_GROUP_ROLE_MAPPINGS</strong></td>
      <td>JSON array mapping LDAP groups to Phoenix roles. <strong>Must contain at least one mapping</strong> (Phoenix will fail to start if empty).<br /><br />Example (grant all authenticated LDAP users MEMBER): <code>[&#123;"group_dn":"*","role":"MEMBER"&#125;]</code></td>
    </tr>
    <tr>
      <td><strong>PHOENIX_LDAP_ATTR_EMAIL</strong></td>
      <td>LDAP attribute containing the user's email address (e.g., <code>mail</code>). Set to <code>null</code> to enable authentication without email (see warning below for additional required settings).</td>
    </tr>
  </tbody>
</table>

### Optional LDAP Configuration

<table data-full-width="false">
   <thead>
      <tr>
         <th width="280">Environment Variable</th>
         <th>Description</th>
      </tr>
   </thead>
   <tbody>
      <tr>
         <td><strong>PHOENIX_LDAP_PORT</strong></td>
         <td>LDAP server port. Defaults to <code>389</code> for StartTLS or <code>636</code> for LDAPS based on <code>PHOENIX_LDAP_TLS_MODE</code>.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_TLS_MODE</strong></td>
         <td>TLS connection mode. Options:<br />• <code>starttls</code> (default): Upgrade plaintext connection to TLS on port 389<br />• <code>ldaps</code>: TLS from connection start on port 636<br />• <code>none</code>: No encryption (testing only, credentials sent in plaintext)</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_TLS_VERIFY</strong></td>
         <td>Verify server TLS certificates. Defaults to <code>true</code>. Should always be <code>true</code> in production to prevent MITM attacks.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_TLS_CA_CERT_FILE</strong></td>
         <td>Path to custom CA certificate file (PEM format) for TLS verification. Use when your LDAP server uses a private/internal CA not in the system trust store.<br /><br />Example: <code>/etc/ssl/certs/internal-ca.pem</code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_TLS_CLIENT_CERT_FILE</strong></td>
         <td>Path to client certificate file (PEM format) for mutual TLS authentication. Requires <code>PHOENIX_LDAP_TLS_CLIENT_KEY_FILE</code> to also be set.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_TLS_CLIENT_KEY_FILE</strong></td>
         <td>Path to client private key file (PEM format) for mutual TLS authentication. Requires <code>PHOENIX_LDAP_TLS_CLIENT_CERT_FILE</code> to also be set.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_BIND_DN</strong></td>
         <td>Service account DN for binding to the LDAP server. Optional. When set, <code>PHOENIX_LDAP_BIND_PASSWORD</code> must also be set.<br /><br />Example: <code>CN=svc-phoenix,OU=Service Accounts,DC=corp,DC=com</code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_BIND_PASSWORD</strong></td>
         <td>Service account password for binding to the LDAP server. Should be stored securely (e.g., in a Kubernetes Secret).</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_USER_SEARCH_FILTER</strong></td>
         <td>LDAP filter for finding users. Use <code>%s</code> as placeholder for the username.<br /><br />Defaults to Active Directory format: <code>(&amp;(objectClass=user)(sAMAccountName=%s))</code><br /><br />OpenLDAP example: <code>(&amp;(objectClass=inetOrgPerson)(uid=%s))</code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_ATTR_DISPLAY_NAME</strong></td>
         <td>LDAP attribute containing user's display name. Defaults to <code>displayName</code>.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_ATTR_MEMBER_OF</strong></td>
         <td>LDAP attribute containing group memberships. Defaults to <code>memberOf</code>. Used when <code>PHOENIX_LDAP_GROUP_SEARCH_FILTER</code> is not set (Active Directory mode).</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_ATTR_UNIQUE_ID</strong></td>
         <td>LDAP attribute containing an immutable unique identifier. <strong>Required when</strong> <code>PHOENIX_LDAP_ATTR_EMAIL</code> <strong>is</strong> <code>null</code> (users are identified by this ID instead of email). Also recommended if you expect user emails to change frequently (e.g., due to name changes or company rebranding).<br /><br />Options:<br />• Active Directory: <code>objectGUID</code><br />• OpenLDAP: <code>entryUUID</code><br />• 389 DS: <code>nsUniqueId</code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_GROUP_SEARCH_BASE_DNS</strong></td>
         <td>JSON array of base DNs for group searches. Required when <code>PHOENIX_LDAP_GROUP_SEARCH_FILTER</code> is set.<br /><br />Example: <code>["ou=groups,dc=example,dc=com"]</code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_GROUP_SEARCH_FILTER</strong></td>
         <td>LDAP filter for finding groups containing a user. Use <code>%s</code> as placeholder for the user identifier.<br /><br /><strong>Two modes:</strong><br />• <strong>AD Mode</strong> (not set, recommended for Active Directory): Reads the <code>memberOf</code> attribute directly from the user entry. AD automatically populates this.<br />• <strong>Search Mode</strong> (set): Searches for groups in <code>PHOENIX_LDAP_GROUP_SEARCH_BASE_DNS</code>. Required for POSIX groups (<code>posixGroup</code>) or when <code>memberOf</code> is unavailable.<br /><br />Example: <code>(&amp;(objectClass=posixGroup)(memberUid=%s))</code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_GROUP_SEARCH_FILTER_USER_ATTR</strong></td>
         <td>LDAP attribute from the user entry to substitute for <code>%s</code> in <code>PHOENIX_LDAP_GROUP_SEARCH_FILTER</code>. Only used when <code>PHOENIX_LDAP_GROUP_SEARCH_FILTER</code> is set.<br /><br />When set: Reads the specified attribute from the user's LDAP entry<br />When not set (default): Uses the login username directly<br /><br /><strong>Understanding group membership attributes:</strong><br />• <strong>POSIX groups</strong> (<code>memberUid</code>): Contains usernames like "jdoe". Use default (login username) or <code>uid</code>.<br />• <strong>groupOfNames</strong> (<code>member</code>): Contains full DNs. Requires <code>distinguishedName</code> (Active Directory only—OpenLDAP does not expose DN as an attribute).<br /><br />Common values:<br />• Not set (default): Uses login username (works for POSIX groups)<br />• <code>uid</code>: Explicitly use uid attribute<br />• <code>distinguishedName</code>: Full DN (Active Directory only)</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_LDAP_ALLOW_SIGN_UP</strong></td>
         <td>Allow automatic user creation on first LDAP login. Defaults to <code>true</code>. Set to <code>false</code> to require pre-provisioned users (created via <code>PHOENIX_ADMINS</code> or the UI before first login). Note: Setting to <code>false</code> requires email to be configured (<code>PHOENIX_LDAP_ATTR_EMAIL</code>), as pre-provisioned users are matched by email address.</td>
      </tr>
   </tbody>
</table>

<Warning>
**LDAP Directories Without Email Addresses**

If your LDAP directory does not have the `mail` attribute (or any email attribute), set `PHOENIX_LDAP_ATTR_EMAIL=null` to enable authentication without email. When this mode is enabled:

* `PHOENIX_LDAP_ATTR_UNIQUE_ID` is **required** — users are identified by this immutable ID instead of email
* `PHOENIX_LDAP_ALLOW_SIGN_UP` must be `true` — users are auto-provisioned on first login (pre-provisioning by email is not possible without email addresses)
* `PHOENIX_ADMINS` cannot be used — use `PHOENIX_LDAP_GROUP_ROLE_MAPPINGS` to assign admin roles instead
* Users will appear in Phoenix without email addresses — the UI will display username instead of email where applicable

```bash
# Example: LDAP without email (Active Directory)
export PHOENIX_LDAP_ATTR_EMAIL=null
export PHOENIX_LDAP_ATTR_UNIQUE_ID=objectGUID
export PHOENIX_LDAP_ALLOW_SIGN_UP=true
export PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='[{"group_dn":"CN=Phoenix Admins,OU=Groups,DC=corp,DC=com","role":"ADMIN"},{"group_dn":"*","role":"MEMBER"}]'
```

```bash
# Example: LDAP without email (OpenLDAP)
export PHOENIX_LDAP_ATTR_EMAIL=null
export PHOENIX_LDAP_ATTR_UNIQUE_ID=entryUUID
export PHOENIX_LDAP_ALLOW_SIGN_UP=true
export PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='[{"group_dn":"cn=admins,ou=groups,dc=example,dc=com","role":"ADMIN"},{"group_dn":"*","role":"MEMBER"}]'
```
</Warning>

### Active Directory Configuration Example

```bash
# Enable authentication
export PHOENIX_ENABLE_AUTH=true
export PHOENIX_SECRET=your-secret-key-at-least-32-chars

# LDAP server connection
export PHOENIX_LDAP_HOST=ldap.corp.example.com
export PHOENIX_LDAP_PORT=389
export PHOENIX_LDAP_TLS_MODE=starttls

# Service account for LDAP queries
export PHOENIX_LDAP_BIND_DN="CN=svc-phoenix,OU=Service Accounts,DC=corp,DC=example,DC=com"
export PHOENIX_LDAP_BIND_PASSWORD="service-account-password"

# User search configuration
export PHOENIX_LDAP_USER_SEARCH_BASE_DNS='["OU=Users,DC=corp,DC=example,DC=com"]'
export PHOENIX_LDAP_USER_SEARCH_FILTER="(&(objectClass=user)(sAMAccountName=%s))"

# Attribute mapping
export PHOENIX_LDAP_ATTR_EMAIL=mail
export PHOENIX_LDAP_ATTR_DISPLAY_NAME=displayName
export PHOENIX_LDAP_ATTR_MEMBER_OF=memberOf

# Group to role mapping
export PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='[{"group_dn":"CN=Phoenix Admins,OU=Groups,DC=corp,DC=example,DC=com","role":"ADMIN"},{"group_dn":"CN=Phoenix Users,OU=Groups,DC=corp,DC=example,DC=com","role":"MEMBER"},{"group_dn":"*","role":"VIEWER"}]'
```

### OpenLDAP Configuration Example

```bash
# Enable authentication
export PHOENIX_ENABLE_AUTH=true
export PHOENIX_SECRET=your-secret-key-at-least-32-chars

# LDAP server connection
export PHOENIX_LDAP_HOST=ldap.example.com
export PHOENIX_LDAP_PORT=636
export PHOENIX_LDAP_TLS_MODE=ldaps

# Service account for LDAP queries
export PHOENIX_LDAP_BIND_DN="cn=readonly,dc=example,dc=com"
export PHOENIX_LDAP_BIND_PASSWORD="readonly-password"

# User search configuration
export PHOENIX_LDAP_USER_SEARCH_BASE_DNS='["ou=people,dc=example,dc=com"]'
export PHOENIX_LDAP_USER_SEARCH_FILTER="(&(objectClass=inetOrgPerson)(uid=%s))"

# Attribute mapping
export PHOENIX_LDAP_ATTR_EMAIL=mail
export PHOENIX_LDAP_ATTR_DISPLAY_NAME=cn

# POSIX group search (when memberOf overlay is not available)
# Setting PHOENIX_LDAP_GROUP_SEARCH_FILTER enables group search mode instead of reading memberOf
export PHOENIX_LDAP_GROUP_SEARCH_BASE_DNS='["ou=groups,dc=example,dc=com"]'
export PHOENIX_LDAP_GROUP_SEARCH_FILTER="(&(objectClass=posixGroup)(memberUid=%s))"
# PHOENIX_LDAP_GROUP_SEARCH_FILTER_USER_ATTR not set - uses login username by default
# This works because POSIX memberUid contains usernames (e.g., "jdoe"), not full DNs

# Group to role mapping
export PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='[{"group_dn":"cn=admins,ou=groups,dc=example,dc=com","role":"ADMIN"},{"group_dn":"*","role":"MEMBER"}]'
```

### Active Directory with Nested Groups

For Active Directory environments using nested groups with the `member` attribute (which contains full DNs):

<Info>
**Recommended approach:** Start with the simpler **AD Mode** (read the user's `memberOf` attribute; leave `PHOENIX_LDAP_GROUP_SEARCH_FILTER` unset). This is usually easiest to configure, but it typically only includes the user's **direct** group memberships (not “group-in-group” / nested groups).

If your authorization relies on **nested groups**, use **search mode** with Active Directory's `LDAP_MATCHING_RULE_IN_CHAIN` (OID `1.2.840.113556.1.4.1941`) as shown below, or flatten group memberships in AD.
</Info>

```bash
# Enable authentication
export PHOENIX_ENABLE_AUTH=true
export PHOENIX_SECRET=your-secret-key-at-least-32-chars

# LDAP server connection
export PHOENIX_LDAP_HOST=ldap.corp.example.com
export PHOENIX_LDAP_TLS_MODE=starttls

# Service account
export PHOENIX_LDAP_BIND_DN="CN=svc-phoenix,OU=Service Accounts,DC=corp,DC=example,DC=com"
export PHOENIX_LDAP_BIND_PASSWORD="service-account-password"

# User search
export PHOENIX_LDAP_USER_SEARCH_BASE_DNS='["OU=Users,DC=corp,DC=example,DC=com"]'
export PHOENIX_LDAP_USER_SEARCH_FILTER="(&(objectClass=user)(sAMAccountName=%s))"

# Nested group search using LDAP_MATCHING_RULE_IN_CHAIN (AD-specific OID)
# The member attribute contains full DNs, so we need distinguishedName
export PHOENIX_LDAP_GROUP_SEARCH_BASE_DNS='["DC=corp,DC=example,DC=com"]'
export PHOENIX_LDAP_GROUP_SEARCH_FILTER="(member:1.2.840.113556.1.4.1941:=%s)"
export PHOENIX_LDAP_GROUP_SEARCH_FILTER_USER_ATTR="distinguishedName"

# Group to role mapping
export PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='[{"group_dn":"CN=Phoenix Admins,OU=Groups,DC=corp,DC=example,DC=com","role":"ADMIN"},{"group_dn":"*","role":"VIEWER"}]'
```

### LDAP with Multiple Servers (Failover)

For high availability, configure multiple LDAP servers:

```bash
export PHOENIX_LDAP_HOST="dc1.corp.com,dc2.corp.com,dc3.corp.com"
```

Phoenix will try each server in order until a successful connection is established.

<Warning>
**Multi-server failover behavior:**

Phoenix assumes all configured servers are **replicas with identical user sets**. Failover only occurs on connection errors:

| Condition | Behavior |
|-----------|----------|
| Server unreachable / timeout | ✅ Tries next server |
| User not found | ❌ Returns immediately (no failover) |
| Invalid password | ❌ Returns immediately (no failover) |

**Not supported:** Multi-domain or multi-forest Active Directory configurations where different users exist on different servers. In these environments, users can only authenticate against the first reachable server that contains their account.

This design provides faster authentication, better security (no probing of multiple servers), and avoids masking infrastructure issues like replication lag.
</Warning>

### LDAP with Custom CA Certificate

When your LDAP server uses a certificate signed by an internal CA:

```bash
export PHOENIX_LDAP_TLS_CA_CERT_FILE=/etc/ssl/certs/internal-ca.pem
```

### LDAP with Mutual TLS (Client Certificates)

For environments requiring client certificate authentication:

```bash
export PHOENIX_LDAP_TLS_CLIENT_CERT_FILE=/etc/ssl/certs/phoenix-client.crt
export PHOENIX_LDAP_TLS_CLIENT_KEY_FILE=/etc/ssl/private/phoenix-client.key
```

### Disabling Password Authentication (LDAP-Only)

To require all users to authenticate via LDAP and disable local password authentication:

```bash
export PHOENIX_DISABLE_BASIC_AUTH=true
export PHOENIX_LDAP_HOST=ldap.corp.example.com
export PHOENIX_LDAP_USER_SEARCH_BASE_DNS='["OU=Users,DC=corp,DC=example,DC=com"]'
export PHOENIX_LDAP_ATTR_EMAIL=mail
export PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='[{"group_dn":"*","role":"MEMBER"}]'
# ... other LDAP settings
```

<Warning>
**LDAP Security Best Practices:**
* Always use TLS encryption (`PHOENIX_LDAP_TLS_MODE=starttls` or `ldaps`) in production
* Always verify TLS certificates (`PHOENIX_LDAP_TLS_VERIFY=true`) in production
* Store the bind password securely (e.g., Kubernetes Secrets, HashiCorp Vault)
* Use a dedicated service account with minimal read-only permissions
* Configure group role mappings to follow the principle of least privilege
</Warning>

<Info>
**User Identity:**
By default, Phoenix identifies LDAP users by their email address. Configure `PHOENIX_LDAP_ATTR_UNIQUE_ID` to use an immutable identifier like `objectGUID` (Active Directory) or `entryUUID` (OpenLDAP) if:
- Your LDAP directory does not have email addresses (set `PHOENIX_LDAP_ATTR_EMAIL=null`)
- You expect user emails to change frequently (e.g., due to company rebranding or name changes)
</Info>

## Advanced Authentication Configuration

The following optional environment variables provide additional control over authentication behavior for advanced use cases:

<table data-full-width="false">
   <thead>
      <tr>
         <th width="240">Variable</th>
         <th>Description</th>
      </tr>
   </thead>
   <tbody>
      <tr>
         <td><strong>PHOENIX_ADMIN_SECRET</strong></td>
         <td>A secret key that can be used as a bearer token instead of an API key. It authenticates as the first system user (admin). This key must be at least 32 characters long, include at least one digit and one lowercase letter, and must be different from <code>PHOENIX_SECRET</code>. Additionally, it must not be set if <code>PHOENIX_SECRET</code> is not configured.<br /><br />Usage: <code>Authorization: Bearer \<PHOENIX_ADMIN_SECRET\></code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_DISABLE_BASIC_AUTH</strong></td>
         <td>Forbid login via password and disable the creation of local users, which log in via passwords. This can be helpful in setups where authentication is handled entirely through OAuth2. Defaults to <code>False</code>.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_DISABLE_BRUTE_FORCE_LOGIN_PROTECTION</strong></td>
         <td>Disable brute force login protection. Defaults to <code>false</code>. Use with caution as this removes protection against repeated failed login attempts.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_BRUTE_FORCE_LOGIN_PROTECTION_MAX_ATTEMPTS</strong></td>
         <td>Maximum number of failed login attempts before temporary lockout. Defaults to <code>5</code>. Lockout duration is 5 minutes.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_ACCESS_TOKEN_EXPIRY_MINUTES</strong></td>
         <td>The duration, in minutes, before access tokens expire. Defaults to the system default if not specified.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_REFRESH_TOKEN_EXPIRY_MINUTES</strong></td>
         <td>The duration, in minutes, before refresh tokens expire. Defaults to the system default if not specified.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_PASSWORD_RESET_TOKEN_EXPIRY_MINUTES</strong></td>
         <td>The duration, in minutes, before password reset tokens expire. Defaults to the system default if not specified.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_ADMINS</strong></td>
         <td>A semicolon-separated list of username and email address pairs to create as admin users on startup. The format is <code>username=email</code>, e.g., <code>John Doe=john@example.com;Doe, Jane=jane@example.com</code>. The password for each user will be randomly generated and will need to be reset. The application will not start if this environment variable is set but cannot be parsed or contains invalid emails. If the username or email address already exists in the database, the user record will not be modified. Changing this environment variable for the next startup will not undo any records created in previous startups.</td>
      </tr>
      <tr>
         <td><strong>PHOENIX_ROOT_URL</strong></td>
         <td>This is the full URL used to access Phoenix from a web browser. This setting is important when you have a reverse proxy in front of Phoenix. If the reverse proxy exposes Phoenix through a sub-path, add that sub-path to the end of this URL setting.<br /><br /><strong>⚠️ WARNING:</strong> When a sub-path is needed, you must also specify the sub-path via the environment variable <code>PHOENIX_HOST_ROOT_PATH</code>. Setting just this URL setting is not enough.<br /><br />Examples:<br />• With a sub-path: <code>https://example.com/phoenix</code><br />• Without a sub-path: <code>https://phoenix.example.com</code></td>
      </tr>
      <tr>
         <td><strong>PHOENIX_MANAGEMENT_URL</strong></td>
         <td>The URL to use for redirecting to a management interface that may be hosting Phoenix. If set, and the current user is within <code>PHOENIX_ADMINS</code>, a link will be added to the navigation menu to return to this URL.</td>
      </tr>
   </tbody>
</table>
