Problem
When Radar runs with --auth-mode=oidc, headless clients — MCP tools, CI pipelines, automation scripts — cannot authenticate because they cannot complete the browser-based OIDC login flow. The only current workaround is manually minting a raw radar_session cookie using the HMAC secret, which is fragile, not user-scoped, and bypasses any per-user RBAC.
Proposed Solution
Add a /api/auth/api-keys endpoint that lets authenticated users generate long-lived API keys tied to their identity (username + groups). The keys are usable via standard headers:
Authorization: Bearer rk_<key>
# or
X-Api-Key: rk_<key>
Keys inherit the creator's OIDC identity so all existing RBAC namespace filtering and permission checks apply unchanged.
Design
- Per-user scoping: each key stores
username and groups from the time of creation. RBAC is unchanged — the key user sees exactly what their OIDC user sees.
- Stateless: API key auth is checked per-request after the session cookie, before proxy headers. No session cookie is set — safe for headless clients.
- Secure storage: only
hex(sha256(plaintext)) is persisted — plaintext is returned exactly once at creation time. SQLite-backed store (same driver as timeline), file created with 0o600.
- Works in all auth modes: key lookup runs regardless of
--auth-mode. Key creation requires a non-none auth mode (no stable user identity to bind to otherwise).
API
| Method |
Path |
Description |
GET |
/api/auth/api-keys |
List caller's keys (no plaintext, no hash) |
POST |
/api/auth/api-keys |
Create key — returns plaintext once in "key" field |
DELETE |
/api/auth/api-keys/{id} |
Revoke key by ID |
Create key
curl -X POST https://radar.example.com/api/auth/api-keys \
-H "Cookie: radar_session=<your-session>" \
-H "Content-Type: application/json" \
-d '{"description": "MCP tool"}'
Response:
{
"id": "rk_2a3b4c...",
"key": "7f8e9d...",
"description": "MCP tool",
"username": "alice",
"groups": ["devs"],
"createdAt": "2026-05-19T10:00:00Z"
}
Use key
curl https://radar.example.com/api/dashboard \
-H "Authorization: Bearer 7f8e9d..."
Configuration
--auth-api-keys-file <path>
Defaults to ~/.config/radar/api-keys.db when --auth-mode is not none.
Files Changed
| File |
Change |
pkg/auth/apikeys.go |
NEW — APIKeyStore + APIKey types, CRUD, SQLite persistence |
pkg/auth/types.go |
Add APIKeys *APIKeyStore field to Config |
internal/auth/auth.go |
Re-export APIKeyStore, APIKey, NewAPIKeyStore |
internal/auth/middleware.go |
API key check between cookie and proxy blocks; extractAPIKey helper |
internal/auth/apikey_handlers.go |
NEW — HandleListAPIKeys, HandleCreateAPIKey, HandleDeleteAPIKey |
internal/server/server.go |
Register 3 routes; add Authorization/X-Api-Key to CORS AllowedHeaders |
cmd/explorer/main.go |
--auth-api-keys-file flag; store init; wire into AuthConfig |
web/src/components/settings/SettingsDialog.tsx |
API Keys section in Settings UI (create, list, revoke) |
Implementation
Working implementation available at: https://github.com/rianovski/radar
Alternatives Considered
- Single static key (
--auth-api-key): simpler, but no per-user scoping and no revocation per user.
- JWT-signed tokens: more portable but adds a signing round-trip and extra dependency.
- JSON file storage: initially tried, switched to SQLite to match existing timeline storage pattern — atomic deletes, no full-file rewrites, proper indexing by username.
Problem
When Radar runs with
--auth-mode=oidc, headless clients — MCP tools, CI pipelines, automation scripts — cannot authenticate because they cannot complete the browser-based OIDC login flow. The only current workaround is manually minting a rawradar_sessioncookie using the HMAC secret, which is fragile, not user-scoped, and bypasses any per-user RBAC.Proposed Solution
Add a
/api/auth/api-keysendpoint that lets authenticated users generate long-lived API keys tied to their identity (username + groups). The keys are usable via standard headers:Keys inherit the creator's OIDC identity so all existing RBAC namespace filtering and permission checks apply unchanged.
Design
usernameandgroupsfrom the time of creation. RBAC is unchanged — the key user sees exactly what their OIDC user sees.hex(sha256(plaintext))is persisted — plaintext is returned exactly once at creation time. SQLite-backed store (same driver as timeline), file created with0o600.--auth-mode. Key creation requires a non-noneauth mode (no stable user identity to bind to otherwise).API
GET/api/auth/api-keysPOST/api/auth/api-keys"key"fieldDELETE/api/auth/api-keys/{id}Create key
Response:
{ "id": "rk_2a3b4c...", "key": "7f8e9d...", "description": "MCP tool", "username": "alice", "groups": ["devs"], "createdAt": "2026-05-19T10:00:00Z" }Use key
curl https://radar.example.com/api/dashboard \ -H "Authorization: Bearer 7f8e9d..."Configuration
Defaults to
~/.config/radar/api-keys.dbwhen--auth-modeis notnone.Files Changed
pkg/auth/apikeys.goAPIKeyStore+APIKeytypes, CRUD, SQLite persistencepkg/auth/types.goAPIKeys *APIKeyStorefield toConfiginternal/auth/auth.goAPIKeyStore,APIKey,NewAPIKeyStoreinternal/auth/middleware.goextractAPIKeyhelperinternal/auth/apikey_handlers.goHandleListAPIKeys,HandleCreateAPIKey,HandleDeleteAPIKeyinternal/server/server.goAuthorization/X-Api-Keyto CORSAllowedHeaderscmd/explorer/main.go--auth-api-keys-fileflag; store init; wire intoAuthConfigweb/src/components/settings/SettingsDialog.tsxImplementation
Working implementation available at: https://github.com/rianovski/radar
Alternatives Considered
--auth-api-key): simpler, but no per-user scoping and no revocation per user.