Skip to Content
ConfigurationUI & API Authentication

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

ProviderSlugNotes
Microsoft Entra IDentraidSupports tenant-based URL shortcuts
GooglegoogleGoogle Workspace / Cloud Identity
GitHubgithubGitHub OAuth Apps
OpenID ConnectoidcAny standards-compliant OIDC provider (Keycloak, ADFS, Okta, Auth0)

Quick Start

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-secret

Google

AUTH_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-secret

Google 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-secret

OpenID 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-secret

AUTH_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-api

AD 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-secret

Take 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=upn

See 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.

VariableDefaultDescription
AUTH_<PROVIDER>_ENABLEDfalseEnable this provider
AUTH_<PROVIDER>_NAMEAutoDisplay 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_TTL15mHow long fetched signing keys are cached
AUTH_<PROVIDER>_SCOPESopenid email profileOAuth scopes (space or comma-separated)
AUTH_<PROVIDER>_USE_PKCEtrueEnable PKCE for authorization code flow
AUTH_<PROVIDER>_USE_REFRESH_TOKENtrueEnable refresh token handling
AUTH_<PROVIDER>_ALLOW_SIGN_UPtrueAllow new users to register
AUTH_<PROVIDER>_AUTO_LOGINfalseAuto-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.

FieldVariableDefault chainUsed by
Username (UPN)AUTH_<PROVIDER>_USERNAME_CLAIMupn, preferred_usernameSession username
EmailAUTH_<PROVIDER>_EMAIL_CLAIMemail, preferred_username, upnSession email, AUTH_<PROVIDER>_ALLOWED_DOMAINS
GroupsAUTH_<PROVIDER>_GROUPS_CLAIMgroups, roles, role, groupSession 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:

  1. Signed JWT - verified against the signing keys the provider publishes.
  2. 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.

ProviderAccess token shapeRequired for bearer auth
Entra IDJWT (aud is api://<client-id>)AUTH_ENTRAID_TENANT or AUTH_ENTRAID_ISSUER
OpenID ConnectJWTAUTH_OIDC_ISSUER
Googleopaque (ya29.*)AUTH_GOOGLE_API_URL, and AUTH_GOOGLE_ISSUER left unset
GitHubopaque (gho_*, ghp_*)AUTH_GITHUB_API_URL

What Is Checked on a Signed JWT

CheckRule
SignatureMust match a key the provider publishes at its JWKS endpoint
AlgorithmRS*, PS*, ES* only. none and the HMAC family are rejected
expRequired. A token without exp is rejected. 60s clock leeway
nbf, iatValidated when present, same leeway
issMust match AUTH_<PROVIDER>_ISSUER. A trailing slash on either side is ignored
aud, azpMust 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_AUDIENCES defaults to AUTH_<PROVIDER>_CLIENT_ID when it is not set. For Entra ID the default is api://<client-id> and <client-id>.
  • Both aud (a string or an array) and azp are examined. A Keycloak token whose aud names a resource server still authenticates when azp is the Pilot client id; a token whose azp names a different application is rejected.
  • A token that names none of the expected audiences is rejected with 403 and the message invalid 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. Setting AUTH_GITHUB_ALLOWED_AUDIENCES therefore rejects every GitHub bearer token with 403 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
VariableDescription
AUTH_<PROVIDER>_ALLOWED_ORGANIZATIONSAllowed organizations (comma-separated)
AUTH_<PROVIDER>_ALLOWED_DOMAINSAllowed email domains (comma-separated)
AUTH_<PROVIDER>_ALLOWED_GROUPSAllowed groups (comma-separated)
AUTH_<PROVIDER>_ALLOWED_AUDIENCESAllowed 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

VariableDefaultDescription
AUTH_COOKIE_SECRET""HMAC signing key for session cookies. Required in production. Auto-generated in dev if empty.
AUTH_COOKIE_NAMEpilot_sessionSession cookie name
AUTH_COOKIE_DOMAIN""Cookie domain scope
AUTH_COOKIE_SECUREtrueHTTPS-only cookies. Set to false for HTTP-only dev environments.
AUTH_SESSION_TTL12hSession time-to-live

Debug Options

VariableDefaultDescription
AUTH_DEBUG_EXPOSE_TOKENSfalseExpose raw access tokens in /api/v1/auth/me response. Never enable in production.

API Endpoints

MethodPathDescription
GET/api/v1/auth/providersList configured OAuth providers
GET/api/v1/auth/login/{provider}Initiate OAuth flow
GET/api/v1/auth/callback/{provider}OAuth callback
GET/api/v1/auth/meCurrent authenticated user
POST/api/v1/auth/logoutEnd session
POST/api/v1/auth/refreshRefresh access token

Entra ID Convenience

When AUTH_ENTRAID_TENANT is set, Pilot automatically derives:

  • AUTH_URL → https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize
  • TOKEN_URL → https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token
  • ISSUER → https://login.microsoftonline.com/<tenant>/v2.0
  • API_URL → https://graph.microsoft.com/oidc/userinfo
  • ALLOWED_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

  1. A user authenticates via OAuth in the Pilot UI
  2. From the user menu, they create a PAT with a name, scope, and optional expiry
  3. The plaintext token (pat_...) is shown once and must be copied immediately
  4. 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

ScopePermissions
readGET operations only (monitoring, viewing configs, browsing messages)
writeAll operations including mutations (reassignments, ACLs, quotas, etc.)

Configuration

VariableDefaultDescription
AUTH_PAT_ENABLEDfalseEnable 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_USER10Maximum tokens per user

Important: Both AUTH_PAT_ENABLED=true and a non-empty AUTH_PAT_HASH_SECRET are required for PATs to work. If the hash secret is not set, PAT endpoints will return a “not enabled” error even when AUTH_PAT_ENABLED=true.

API Endpoints

MethodPathDescription
GET/api/v1/tokensList your tokens
POST/api/v1/tokensCreate 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
Last updated on