Authentication API
Use Keycloak OIDC for browser sign-in and panel API keys for automation. These are panel interfaces, separate from Partner Center's OIDC login and pcp_ tokens. The router also retains legacy password/JWT endpoints; do not mistake POST /api/v1/auth/login for the OIDC browser entry point.
OIDC Login Flow
The browser opens GET /api/v1/auth/oidc/login, which redirects to Keycloak using Authorization Code with PKCE. Keycloak returns to GET /api/v1/auth/oidc/callback; the panel exchanges the code, creates a session, sets oidc_session, and redirects to its configured frontend callback. The login handler does not implement a caller-selected redirect query parameter.
| Endpoint | Purpose |
|---|---|
GET /api/v1/auth/oidc-config | Frontend OIDC configuration |
GET /api/v1/auth/oidc/login | Browser sign-in redirect; 503 if OIDC is unconfigured |
GET /api/v1/auth/oidc/callback | Provider callback, not a direct integration call |
POST /api/v1/auth/oidc/logout | Clears the cookie, attempts session-row deletion, returns redirect_url for the browser to complete Keycloak logout |
GET /api/v1/auth/me | Authenticated identity, wrapped in user |
Example identity response (synthetic UUID and email):
{
"user": {
"id": "10000000-0000-4000-8000-000000000001",
"email": "[email protected]",
"role": "admin"
}
}
Logout's JSON response alone does not complete the browser's Keycloak logout; the browser follows redirect_url. Do not assume it revokes every other session.
API Keys
Create a key in Settings → Security → API Keys, or call POST /api/v1/api-keys using an already authenticated session or authorized key. The full secret is returned once. Keep it in a secrets manager and revoke it if lost or exposed.
Creating an API Key
curl -X POST https://panel.example.com/api/v1/api-keys \
-H "Authorization: Bearer ${PANEL_API_KEY}" \
-H 'Content-Type: application/json' \
-d '{"name":"inventory","role":"admin","rateLimitRpm":60}'
| Field | Contract |
|---|---|
name | Required |
role | admin or customer; defaults to the caller's role. A customer cannot mint an admin key. |
scopes | Optional string array; specialized tenant MCP keys use the MCP surface below. |
permissionsDeny | Optional array of known permission names |
expiresAt | Optional future RFC3339 timestamp; omitted means no expiry |
rateLimitRpm | Positive requests/minute; defaults to 60 |
isServiceAccount, description | Optional metadata |
201 Created returns key and metadata, rather than a flat key-record object:
{
"key": "REDACTED_PANEL_API_KEY",
"metadata": {
"name": "inventory",
"scopes": null,
"role": "admin",
"permissionsDeny": null,
"rateLimitRpm": 60,
"expiresAt": null,
"isServiceAccount": false,
"description": ""
}
}
The redacted value above is not usable. Customer callers also go through the api_keys capability, organization attachment, and key-count policy; successful customer minting includes orgId in metadata. Admin-created keys remain platform-scoped on this route, including admin-created customer-role keys. A role label alone does not select a tenant.
A customer caller must hold, in its organization, every permission of the key's role; otherwise the response is 403 with the code insufficient_role and no key is created. A key bound to an organization works only for that organization and the ones below it, with the key's own role, cut down to what its creator holds there now, and answers 404 for everything else. Called with such a key, POST /api-keys creates keys only for the same organization, and GET /api-keys and DELETE /api-keys/{id} see only the creator's keys bound to it. One exception until the next release: /secrets* and /portal/* take the customer from the key's creator, not from the key, so there a key still follows its creator (AI-1055, slice 2). See API Keys.
Using an API Key
curl https://panel.example.com/api/v1/services \
-H "Authorization: Bearer ${PANEL_API_KEY}"
Choose the role and endpoint for the intended account. Admin service/template routes require admin; customer integrations use the portal API. Permission-deny settings affect routes that enforce those permissions and should not be treated as a universal read-only replacement for an admin key.
Listing API Keys
GET /api/v1/api-keys returns {"keys": [...]} for the authenticated user. Each record includes its UUID id, name, prefix, role, scopes, policy metadata, and timestamps; the full key is not returned. Use that UUID when revoking.
Revoking an API Key
DELETE /api/v1/api-keys/{id} revokes a key owned by the authenticated user and returns 204 No Content. Subsequent authentication with the revoked key fails.
Session Management
Security limitation in v2.12.8: The OIDC session implementation stores access and refresh tokens using repeating-key XOR, without authenticated encryption. Do not rely on this storage format to protect tokens if the session database is exposed. Protect database and backup access; this limitation needs separate product-security remediation.
Session Storage
OIDC sessions are stored in PostgreSQL's identity_sessions table. The browser receives an opaque session identifier in the oidc_session cookie. Its attributes are:
HttpOnly— not accessible via JavaScriptSecure— only sent over HTTPSSameSite=Lax— limits when browsers send the cookie across sitesPath=/; when configured, the cookie domain also covers service subdomains
Session Expiry
The login callback sets the cookie lifetime and server-side session expiry from Keycloak's token response (expires_in). The request middleware checks that stored expiry; it does not extend it on each request. Sign in again when the session expires.
Role-Based Access
The main services and templates routes in this reference belong to the admin-only router group. Customer access is served by /api/v1/portal routes with customer/organization checks. There is no operator or viewer grant for these admin routes. Do not infer tenant isolation from an optional customerId field on an admin request: that field assigns a resource's customer.
MCP Server describes the separate admin and tenant MCP endpoints and the credentials each accepts.