Skip to content

User-minted API keys for headless integrations - #35

Merged
KodeStar merged 3 commits into
mainfrom
feat/api-keys
Jul 9, 2026
Merged

User-minted API keys for headless integrations#35
KodeStar merged 3 commits into
mainfrom
feat/api-keys

Conversation

@KodeStar

@KodeStar KodeStar commented Jul 9, 2026

Copy link
Copy Markdown
Owner

What

Named, individually revocable, non-expiring API keys (personal access tokens) a user mints for headless consumers - dashboards (Heimdall), cron, monitoring - so no integration ever stores the admin username+password again.

  • New token kind api alongside session/pairing (no migration - tokens.kind is free-text).
  • POST /api/v1/auth/tokens (mint; secret returned exactly once, stored as SHA-256), GET /api/v1/auth/tokens (owner's keys, metadata only, newest first), DELETE /api/v1/auth/tokens/{id} (owner-scoped revoke). All three share the account rate limit and refuse demo accounts, matching the recovery/password handlers.
  • requireAuth/requireMediaAuth now resolve session or api kinds (ResolveTokenKinds); a key acts as its owner (an admin's key passes /admin/*, a non-admin's does not). Pairing tokens are still never accepted as bearer auth, and an api key is never valid for /auth/exchange.
  • GET /server capabilities gain api_keys: true so clients can gate the management UI.

Tests

13 new tests: mint-and-use on /admin/stats, revoked key rejected, pairing-token-as-bearer rejected, key-on-exchange rejected, non-admin key 403 on admin routes, cross-user revoke 404, demo refusal, empty label 400, metadata-only list, capability flag, media ?token= acceptance, plus auth-layer lifecycle/scoping units.

Full gate green: go build && go vet && go test -race && golangci-lint run. Also verified live end-to-end: mint via API -> Authorization: Bearer <key> on GET /api/v1/admin/stats -> 200; revoke -> 401.

Pairing

Wire change shipped with the frontend management UI and docs in sibling PRs (links to follow).

Named, individually revocable, non-expiring bearer tokens (kind=api) so
integrations (dashboards, cron, monitoring) no longer need a stored
username+password:

- auth: KindAPI; IssueAPIToken (secret shown once, stored hashed);
  ListAPITokens (metadata only); owner-scoped RevokeTokenByID;
  ResolveToken generalized to ResolveTokenKinds (kind IN ...).
- middleware: requireAuth/requireMediaAuth accept session OR api kinds;
  pairing tokens remain excluded, and an api key is never valid for
  exchange.
- api: POST/GET /api/v1/auth/tokens + DELETE /api/v1/auth/tokens/{id},
  rate-limited and refused for demo accounts like recovery/password;
  GET /server advertises the api_keys capability.
- no migration needed: tokens.kind is free-text.

Tests cover the allowed and denied paths: revoked key, pairing token as
bearer, key on exchange, cross-user revoke, demo refusal, non-admin key
on /admin/*, media ?token= acceptance.
@KodeStar

KodeStar commented Jul 9, 2026

Copy link
Copy Markdown
Owner Author

Sibling PRs: frontend KodeStar/audiosilo-frontend#55 · docs KodeStar/audiosilo-docs#10 · workspace glue KodeStar/audiosilo#4

KodeStar and others added 2 commits July 9, 2026 15:20
Extract the per-IP account rate-limit + demo-account refusal shared by
handleSetPassword, handleGenerateRecovery and the API-key endpoints into
gateSelfService so the guard lives in one place. Add constant-string fast
paths to inPlaceholders for the 1- and 2-kind cases the per-request auth
path hits, so lookupToken adds no allocation there.
An API key acts as its owner, but a leaked key could previously mint a
fresh credential that outlived its own revocation - another API key
(POST /auth/tokens), a recovery code (POST /auth/recovery), a pairing
token (POST /auth/pair -> exchange), or a first password on a
password-less account (POST /auth/password). Revoking the leaked key
then failed to contain the compromise.

Enforce a containment invariant: revoking a key cuts off everything it
could reach. ResolveTokenKinds now returns the matched token kind;
middleware stashes it in request context; denyAPIKey refuses an api-key
caller (403) on the four durable-credential-minting routes. A key may
still list/revoke keys and clear a recovery code (those only reduce
access), and a session is unaffected. Mirrors GitHub's "a token cannot
create tokens".

Tests: TestAPIKeyCannotMintDurableCredentials (denied x4 for a key,
allowed for a session, list/revoke still allowed); auth-layer assertions
that the resolver reports kind=api.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KodeStar
KodeStar merged commit 2f769a0 into main Jul 9, 2026
2 checks passed
@KodeStar
KodeStar deleted the feat/api-keys branch July 9, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant