UI & API Authentication
Pilot supports OAuth2/OIDC authentication to restrict access to the dashboard and API. When enabled, users must authenticate through a configured identity provider before accessing Pilot.
Note: This page covers authentication for the Pilot UI and API. For Kafka broker authentication, see SASL and OAuth for Kafka.
Supported Providers
| Provider | Slug | Notes |
|---|---|---|
| Microsoft Entra ID | entraid | Supports tenant-based URL shortcuts |
google | Google Workspace / Cloud Identity | |
| GitHub | github | GitHub OAuth Apps |
| OpenID Connect | oidc | Any standards-compliant OIDC provider (Keycloak, ADFS, Okta, Auth0) |
Quick Start
Entra ID (Recommended for Enterprise)
Entra ID has a convenience shortcut - set AUTH_ENTRAID_TENANT and Pilot auto-derives the auth, token, and issuer URLs:
AUTH_ENTRAID_ENABLED=true
AUTH_ENTRAID_TENANT=your-tenant-id
AUTH_ENTRAID_CLIENT_ID=your-app-registration-client-id
AUTH_ENTRAID_CLIENT_SECRET=your-client-secret
AUTH_COOKIE_SECRET=random-32-char-secretAUTH_GOOGLE_ENABLED=true
AUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
AUTH_GOOGLE_CLIENT_SECRET=your-client-secret
AUTH_GOOGLE_AUTH_URL=https://accounts.google.com/o/oauth2/v2/auth
AUTH_GOOGLE_TOKEN_URL=https://oauth2.googleapis.com/token
AUTH_GOOGLE_API_URL=https://openidconnect.googleapis.com/v1/userinfo
AUTH_COOKIE_SECRET=random-32-char-secretGoogle access tokens are opaque, so AUTH_GOOGLE_ISSUER is deliberately absent above.
Setting it makes Pilot verify the id_token signature at browser login, but it also
turns off bearer auth for every Google access token, since Pilot then binds credentials
by audience and an opaque token declares none. Add it only if no Google bearer client
(MCP, CI) authenticates against this deployment. See the callout under
Bearer Token Validation for why the two are mutually exclusive.
GitHub
AUTH_GITHUB_ENABLED=true
AUTH_GITHUB_CLIENT_ID=your-github-oauth-app-client-id
AUTH_GITHUB_CLIENT_SECRET=your-client-secret
AUTH_GITHUB_AUTH_URL=https://github.com/login/oauth/authorize
AUTH_GITHUB_TOKEN_URL=https://github.com/login/oauth/access_token
AUTH_GITHUB_API_URL=https://api.github.com/user
AUTH_COOKIE_SECRET=random-32-char-secretOpenID Connect (Keycloak, ADFS, Okta, Auth0)
The oidc provider is vendor-neutral - point it at any standards-compliant OIDC issuer. Keycloak:
AUTH_OIDC_ENABLED=true
AUTH_OIDC_NAME=Keycloak
AUTH_OIDC_CLIENT_ID=pilot
AUTH_OIDC_CLIENT_SECRET=your-client-secret
AUTH_OIDC_AUTH_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/auth
AUTH_OIDC_TOKEN_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/token
AUTH_OIDC_API_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/userinfo
AUTH_OIDC_ISSUER=https://keycloak.example.com/realms/your-realm
AUTH_COOKIE_SECRET=random-32-char-secretAUTH_OIDC_NAME sets the label on the login button. The callback URL Pilot
advertises is https://<pilot-host>/api/v1/auth/callback/oidc - register that
exact URI with your provider.
Keycloak access tokens often carry an aud of account and the client id in azp. Pilot accepts either, so no extra setting is needed for the common case. If your realm maps a dedicated resource-server audience onto Pilot’s tokens, list it explicitly:
AUTH_OIDC_ALLOWED_AUDIENCES=pilot-apiAD FS
AD FS 2016 or newer (earlier versions are WS-Federation/SAML only, which Pilot does not support):
AUTH_OIDC_ENABLED=true
AUTH_OIDC_NAME=ADFS
AUTH_OIDC_CLIENT_ID=<adfs-client-id>
AUTH_OIDC_CLIENT_SECRET=<adfs-client-secret>
AUTH_OIDC_AUTH_URL=https://adfs.example.com/adfs/oauth2/authorize
AUTH_OIDC_TOKEN_URL=https://adfs.example.com/adfs/oauth2/token
AUTH_OIDC_ISSUER=https://adfs.example.com/adfs
AUTH_COOKIE_SECRET=random-32-char-secretTake the exact issuer, authorization_endpoint, and token_endpoint values
from https://<adfs-host>/adfs/.well-known/openid-configuration; the issuer is
compared exactly. Register the AD FS application as a Server application so
it accepts a client secret in the token request body.
On AD FS 2016, also set AUTH_OIDC_USE_PKCE=false - PKCE arrived in AD FS 2019,
and older versions reject the code_verifier parameter.
AD FS claim names depend on the relying party’s issuance transform rules - there
is no built-in groups claim, and the userinfo endpoint returns only sub, so
Pilot reads claims from the id_token. Group membership commonly arrives under
roles or role (Token-Groups mapped to the Role claim type). Decode a token
and point the mapping at what your rules issue:
AUTH_OIDC_GROUPS_CLAIM=roles
AUTH_OIDC_USERNAME_CLAIM=upnSee Claim Mapping for the defaults.
Per-Provider Configuration
All providers use the same configuration pattern with the AUTH_<PROVIDER>_* prefix. Replace <PROVIDER> with ENTRAID, GOOGLE, GITHUB, or OIDC.
AUTH_<PROVIDER>_ENABLED must be present in the environment for the rest of that provider’s block to be read at all. Setting only AUTH_OIDC_CLIENT_ID, for example, configures nothing.
| Variable | Default | Description |
|---|---|---|
AUTH_<PROVIDER>_ENABLED | false | Enable this provider |
AUTH_<PROVIDER>_NAME | Auto | Display name on login page |
AUTH_<PROVIDER>_CLIENT_ID | "" | OAuth client ID |
AUTH_<PROVIDER>_CLIENT_SECRET | "" | OAuth client secret |
AUTH_<PROVIDER>_AUTH_URL | "" | Authorization endpoint |
AUTH_<PROVIDER>_TOKEN_URL | "" | Token endpoint |
AUTH_<PROVIDER>_API_URL | "" | Userinfo endpoint |
AUTH_<PROVIDER>_ISSUER | "" | OIDC issuer. Enables JWKS discovery and bearer JWT signature verification |
AUTH_<PROVIDER>_JWKS_URL | "" | JWKS endpoint. Overrides discovery from the issuer, but still requires it. Must be an absolute http/https URL; anything else is ignored |
AUTH_<PROVIDER>_JWKS_CACHE_TTL | 15m | How long fetched signing keys are cached |
AUTH_<PROVIDER>_SCOPES | openid email profile | OAuth scopes (space or comma-separated) |
AUTH_<PROVIDER>_USE_PKCE | true | Enable PKCE for authorization code flow |
AUTH_<PROVIDER>_USE_REFRESH_TOKEN | true | Enable refresh token handling |
AUTH_<PROVIDER>_ALLOW_SIGN_UP | true | Allow new users to register |
AUTH_<PROVIDER>_AUTO_LOGIN | false | Auto-redirect to provider (skip login page) |
AUTH_<PROVIDER>_USERNAME_CLAIM | "" | Claim for the session username. See Claim Mapping |
AUTH_<PROVIDER>_EMAIL_CLAIM | "" | Claim for the session email and ALLOWED_DOMAINS. See Claim Mapping |
AUTH_<PROVIDER>_GROUPS_CLAIM | "" | Claim for session groups and ALLOWED_GROUPS. See Claim Mapping |
Claim Mapping
Pilot builds the session identity (username, email, groups) from the claims the provider returns. Each field has a default chain of claim names, tried in order; the first one present with a non-empty value wins.
| Field | Variable | Default chain | Used by |
|---|---|---|---|
| Username (UPN) | AUTH_<PROVIDER>_USERNAME_CLAIM | upn, preferred_username | Session username |
AUTH_<PROVIDER>_EMAIL_CLAIM | email, preferred_username, upn | Session email, AUTH_<PROVIDER>_ALLOWED_DOMAINS | |
| Groups | AUTH_<PROVIDER>_GROUPS_CLAIM | groups, roles, role, group | Session groups, AUTH_<PROVIDER>_ALLOWED_GROUPS |
A configured claim is consulted first, with the default chain as fallback. The groups claim accepts an array or a single string; lists are never merged across claim names.
Claims come from both the userinfo response and the id_token (userinfo wins; the id_token fills in what is missing), so a provider whose userinfo returns only sub still yields a full identity. Make sure the IdP emits the claim you filter on into the token; see Access Control.
Bearer Token Validation
API clients that cannot hold a session cookie authenticate with Authorization: Bearer <token>. Pilot accepts a bearer credential in exactly two ways:
- Signed JWT - verified against the signing keys the provider publishes.
- Opaque token - presented to
AUTH_<PROVIDER>_API_URL. The response is the identity; if the provider does not answer with a claim set, the request is rejected.
There is no third path. A bearer token’s own payload is never an identity: a token whose signature cannot be checked, and that no userinfo endpoint vouches for, is rejected with 401.
Opaque tokens require a provider that cannot verify JWTs. A userinfo response
names a user, never the client the token was minted for, so Pilot cannot tell one
application’s opaque token from another’s. Where a provider publishes an issuer,
Pilot binds credentials by audience instead, and an opaque token from that provider
is rejected with 403 invalid audience. Accepting it would let any token the issuer
minted for any other client authenticate, which would make the audience check
optional rather than mandatory.
In practice: setting AUTH_<PROVIDER>_ISSUER turns off opaque-token bearer auth for
that provider. Browser login and JWT bearer auth are unaffected. GitHub publishes no
issuer, so its tokens are always authenticated through userinfo.
| Provider | Access token shape | Required for bearer auth |
|---|---|---|
| Entra ID | JWT (aud is api://<client-id>) | AUTH_ENTRAID_TENANT or AUTH_ENTRAID_ISSUER |
| OpenID Connect | JWT | AUTH_OIDC_ISSUER |
opaque (ya29.*) | AUTH_GOOGLE_API_URL, and AUTH_GOOGLE_ISSUER left unset | |
| GitHub | opaque (gho_*, ghp_*) | AUTH_GITHUB_API_URL |
What Is Checked on a Signed JWT
| Check | Rule |
|---|---|
| Signature | Must match a key the provider publishes at its JWKS endpoint |
| Algorithm | RS*, PS*, ES* only. none and the HMAC family are rejected |
exp | Required. A token without exp is rejected. 60s clock leeway |
nbf, iat | Validated when present, same leeway |
iss | Must match AUTH_<PROVIDER>_ISSUER. A trailing slash on either side is ignored |
aud, azp | Must name this deployment (see Audience Binding) |
Signing keys are discovered at <issuer>/.well-known/openid-configuration, cached for AUTH_<PROVIDER>_JWKS_CACHE_TTL (default 15m), and re-fetched when a token names an unknown kid, so key rotation needs no restart. Discovery and JWKS fetches will not follow a redirect to a different host.
Signing keys being unavailable is a rejection, never a downgrade. A provider configured to verify signatures does not fall back to an unverified token body when its JWKS endpoint is unreachable. Last-known-good keys keep serving for up to 30 minutes past the cache TTL so a transient outage does not lock everyone out; past that window, bearer JWTs for that provider are rejected until the keys are reachable again.
Audience Binding
A valid signature and a matching issuer prove who signed a token, not who it was for. An issuer that signs tokens for several applications (a shared Entra tenant, a multi-client Keycloak realm) would otherwise let a token minted for a different application authenticate here. Pilot therefore requires every bearer JWT to name this deployment, and fails closed when it cannot.
AUTH_<PROVIDER>_ALLOWED_AUDIENCESdefaults toAUTH_<PROVIDER>_CLIENT_IDwhen it is not set. For Entra ID the default isapi://<client-id>and<client-id>.- Both
aud(a string or an array) andazpare examined. A Keycloak token whoseaudnames a resource server still authenticates whenazpis the Pilot client id; a token whoseazpnames a different application is rejected. - A token that names none of the expected audiences is rejected with
403and the messageinvalid audience. - A provider that resolves no audience at all (no client id and no allowlist) rejects every bearer JWT with
401. - A configured allowlist is always enforced. Pilot distinguishes an allowlist it derived from
the client id from one you set in
AUTH_<PROVIDER>_ALLOWED_AUDIENCES. The derived form is waived for a provider that cannot verify JWTs, because a userinfo response names a user and never an audience, so nothing could ever satisfy it. A value you configured is a stated requirement and is never waived, even for such a provider, and even when it happens to equal the value Pilot would have derived. SettingAUTH_GITHUB_ALLOWED_AUDIENCEStherefore rejects every GitHub bearer token with403 invalid audience: GitHub declares no audience, so the requirement cannot be met. Leave it unset for userinfo-only providers.
Access Control
Restrict access by organization, email domain, group, or audience:
AUTH_ENTRAID_ALLOWED_ORGANIZATIONS=your-tenant-id
AUTH_ENTRAID_ALLOWED_DOMAINS=example.com,corp.example.com
AUTH_ENTRAID_ALLOWED_GROUPS=kafka-admins,platform-team
AUTH_ENTRAID_ALLOWED_AUDIENCES=api://your-client-id| Variable | Description |
|---|---|
AUTH_<PROVIDER>_ALLOWED_ORGANIZATIONS | Allowed organizations (comma-separated) |
AUTH_<PROVIDER>_ALLOWED_DOMAINS | Allowed email domains (comma-separated) |
AUTH_<PROVIDER>_ALLOWED_GROUPS | Allowed groups (comma-separated) |
AUTH_<PROVIDER>_ALLOWED_AUDIENCES | Allowed token audiences (comma-separated). Defaults to AUTH_<PROVIDER>_CLIENT_ID (Entra ID also accepts api://<client-id>) so a bearer JWT must be minted for this deployment. |
If any filter is set, users must match at least one value in that filter to be granted access. All four filters fail closed: a token that carries no claim the filter can read (for example a domain restriction against a token with no email) is rejected, not waved through. A failed filter returns 403 with the reason (organization not allowed, email domain not allowed, group not allowed, invalid audience).
The filters run on browser login and on bearer authentication, always against claims that were verified or returned by the provider, never against values read out of an unverified token body. For a signed JWT those are the token’s own verified claims (groups, email, tid, aud); a group or domain the provider exposes only on its userinfo endpoint is not consulted, so make sure the IdP emits the claim you filter on into the token itself. PATs are not subject to these filters.
The group and domain filters read the claim names configured by AUTH_<PROVIDER>_GROUPS_CLAIM and AUTH_<PROVIDER>_EMAIL_CLAIM, falling back to the default chains described under Claim Mapping. A provider that issues groups under a name outside the default chain needs AUTH_<PROVIDER>_GROUPS_CLAIM set for ALLOWED_GROUPS to see them. For AD FS, which issues them under roles, AUTH_<PROVIDER>_GROUPS_CLAIM=roles makes the mapping explicit and consults roles ahead of any groups claim the token may also carry.
Session Settings
| Variable | Default | Description |
|---|---|---|
AUTH_COOKIE_SECRET | "" | HMAC signing key for session cookies. Required in production. Auto-generated in dev if empty. |
AUTH_COOKIE_NAME | pilot_session | Session cookie name |
AUTH_COOKIE_DOMAIN | "" | Cookie domain scope |
AUTH_COOKIE_SECURE | true | HTTPS-only cookies. Set to false for HTTP-only dev environments. |
AUTH_SESSION_TTL | 12h | Session time-to-live |
Debug Options
| Variable | Default | Description |
|---|---|---|
AUTH_DEBUG_EXPOSE_TOKENS | false | Expose raw access tokens in /api/v1/auth/me response. Never enable in production. |
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/auth/providers | List configured OAuth providers |
GET | /api/v1/auth/login/{provider} | Initiate OAuth flow |
GET | /api/v1/auth/callback/{provider} | OAuth callback |
GET | /api/v1/auth/me | Current authenticated user |
POST | /api/v1/auth/logout | End session |
POST | /api/v1/auth/refresh | Refresh access token |
Entra ID Convenience
When AUTH_ENTRAID_TENANT is set, Pilot automatically derives:
AUTH_URL→https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorizeTOKEN_URL→https://login.microsoftonline.com/<tenant>/oauth2/v2.0/tokenISSUER→https://login.microsoftonline.com/<tenant>/v2.0API_URL→https://graph.microsoft.com/oidc/userinfoALLOWED_ORGANIZATIONS→[<tenant>](if not explicitly set)ALLOWED_AUDIENCES→[api://<client-id>, <client-id>](if not explicitly set)
You can override any of these by setting the explicit variable.
Every other provider (Google, GitHub, OIDC) also defaults ALLOWED_AUDIENCES to [<client-id>] when it is not set, so a bearer JWT signed by the trusted issuer for a different application is rejected rather than accepted on issuer alone.
Personal Access Tokens (PATs)
When authentication is enabled, any programmatic access to the Pilot API requires authentication. Personal Access Tokens provide long-lived, revocable Bearer tokens that work with every API endpoint - REST calls, CI/CD pipelines, monitoring integrations, and MCP clients alike.
How It Works
- A user authenticates via OAuth in the Pilot UI
- From the user menu, they create a PAT with a name, scope, and optional expiry
- The plaintext token (
pat_...) is shown once and must be copied immediately - The token is used as a Bearer token:
Authorization: Bearer pat_...
Only the HMAC-SHA256 hash of the token is stored - the plaintext is never persisted.
Token Scopes
| Scope | Permissions |
|---|---|
read | GET operations only (monitoring, viewing configs, browsing messages) |
write | All operations including mutations (reassignments, ACLs, quotas, etc.) |
Configuration
| Variable | Default | Description |
|---|---|---|
AUTH_PAT_ENABLED | false | Enable PAT feature (requires auth to be enabled) |
AUTH_PAT_HASH_SECRET | "" | Required. HMAC-SHA256 secret for hashing tokens. Must be a stable, persistent value - PATs become invalid if this changes. |
AUTH_PAT_MAX_PER_USER | 10 | Maximum tokens per user |
Important: Both
AUTH_PAT_ENABLED=trueand a non-emptyAUTH_PAT_HASH_SECRETare required for PATs to work. If the hash secret is not set, PAT endpoints will return a “not enabled” error even whenAUTH_PAT_ENABLED=true.
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/tokens | List your tokens |
POST | /api/v1/tokens | Create a new token |
DELETE | /api/v1/tokens/{id} | Revoke a token |
Security: PAT management endpoints require an OAuth session. A PAT cannot create or revoke other PATs, preventing token-chaining.
Usage
Include the token as a Bearer token in any API request:
curl https://pilot.example.com/api/v1/cluster \
-H "Authorization: Bearer pat_your_token_here"All API endpoints accept PAT authentication - cluster info, partitions, consumer groups, proposals, ACLs, quotas, and more. See the API Reference for the full endpoint list.
Use Cases
- REST API access - authenticate any API call from scripts, tools, or applications
- CI/CD pipelines - automate Kafka operations (topic config updates, ACL management, reassignments)
- Monitoring integrations - read-only tokens for external dashboards pulling cluster metrics
- MCP clients (GitHub Copilot, Warp, Claude Desktop) - see MCP Authentication for client configuration examples
- Service accounts - long-lived access for automation tools