Skip to content

feat(auth): per-user API keys for programmatic access (MCP, CI, headless clients) #733

Description

@rianovski

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 NEWAPIKeyStore + 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 NEWHandleListAPIKeys, 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions