Multi-Key Authentication
Multiple named API keys per account with per-key audit trail, last-key lockout protection, and soft-delete revocation.
The model
Every CueAPI account holds a collection of API keys, not a single one. Each key is a row in api_keys with:
- a globally unique hash used for authentication
- a short prefix (e.g.
cue_sk_a3f9) shown in the dashboard for identification - a user-provided label (e.g.
claude-desktop,ci-server,github-actions) - a creation source (user / register / migration / mcp-exchange)
- a last-used timestamp and an optional revocation timestamp
Auth resolves a bearer token against api_keys.key_hash. Revoked keys return 401 key_revoked — a distinct error code from invalid_api_key so clients can surface the right message.
Why multiple keys
Rotation without downtime. Mint the new key, update every consumer, revoke the old key. No window where nothing authenticates.
Per-agent credentials. Each MCP host, CI pipeline, worker daemon, or script gets its own named key. When one leaks, you revoke exactly that key — nothing else stops working.
Audit granularity. Every key's last_used_at and creation source are tracked in api_key_audit_log. "Which key did this request come from" is answerable, not just "which user."
Claude Projects isolation. For the remote MCP flow (Stage C of CueAPI's roadmap), each Claude.ai Custom Connector authorization produces its own key labeled mcp:{client}:{connection-id}. Different Projects get different keys, so their cues and executions stay partitioned.
Lifecycle
Creation
Two sources:
-
Registration.
POST /v1/auth/registercreates a user row AND a defaultapi_keysrow withlabel="default",created_by="register". The plaintext is returned in the response once. -
Explicit.
POST /v1/auth/keyswith a label mints a new key (rate-limited to 10/user/hour). Also returns plaintext once. -
MCP exchange.
POST /v1/auth/mcp-exchange(see MCP docs) mints a key for a remote MCP OAuth flow. Labeledmcp:{client_id}:{label}automatically.
Revocation
DELETE /v1/auth/keys/{id} with X-Confirm-Destructive: true flips revoked_at to now. The key stops authenticating immediately (Redis auth cache is invalidated synchronously).
Last-key protection: the API refuses to revoke the user's last non-revoked key with 409 last_key_protected. Users can't lock themselves out by accident.
Soft-delete grace
Revoked rows stay in the database for 30 days so:
GET /v1/auth/keysstill lists them with theirrevoked_attimestampapi_key_audit_logrows referring to them keep their target- A surprise "we need the audit trail back" request has a 30-day window to recover
After 30 days, a cleanup job hard-deletes the row and writes a final api_key_audit_log entry with event_type="hard_deleted".
Legacy regenerate
POST /v1/auth/key/regenerate still works for single-key accounts — it rotates the one non-revoked key in place (updates both the api_keys row and the legacy users.api_key_hash column for backward compatibility). When the account has more than one key, regenerate returns 409 multiple_keys asking you to use POST /v1/auth/keys + DELETE instead (the rotate-without-downtime pattern).
Audit trail
Every lifecycle event writes a row to api_key_audit_log:
event_type | When |
|---|---|
created | Key minted (register / POST keys / mcp-exchange) |
renamed | Label changed via PATCH |
regenerated | Single-key account used legacy /key/regenerate |
revoked | Soft-delete via DELETE |
consumed_by_mcp_exchange | Minted specifically for a remote MCP OAuth flow |
hard_deleted | Cleanup job removed the row after 30-day grace |
Each row captures IP, user-agent, and a JSONB metadata blob. The log is append-only — no update, no delete — and survives the key row itself (via ON DELETE SET NULL on api_key_id).
Backward compatibility
The users.api_key_hash column still exists and still holds a valid hash for every pre-multi-key user. Auth reads from api_keys first; if no row matches, it falls back to the legacy column. That means:
- Existing CLI installations and workers continue authenticating without any config change
- An emergency rollback (revert
auth.pyto the old query) works — the legacy column still has the same value for every user who pre-dates multi-key scoping - Tests that create users directly (bypassing registration) still auth, so test suites don't break
Full detail of the backward-compat contract lives in migration 039 on cueapi-core.
Related
- List API Keys
- Create API Key
- Rename API Key
- Revoke API Key
- MCP Server Overview — where per-agent keys get put to work