Personal Access Tokens for non-browser clients #49

Closed
opened 2026-06-14 19:52:00 +00:00 by james · 0 comments
Owner

Add a per-user, revocable token primitive so external agent runtimes (Claude Code, opencode, custom MCP clients) can authenticate to Carol without holding a session cookie.

Carol's current auth (ADR-0004) is server-side opaque sessions, browser-cookie only. That works perfectly for the PWA but doesn't translate to a CLI / desktop / headless client. A PAT system slots in alongside sessions: the auth middleware authorises a request whose Authorization: Bearer <token> header carries a valid PAT exactly as it would a valid session cookie, with the same per-user data isolation downstream.

Scope

  • New personal_access_tokens table: (id, user_id, token_hash, name, scopes, created_at, last_used_at, expires_at, revoked_at). Tokens are server-generated, returned to the user exactly once at creation, and stored hashed (argon2id, the same primitive we use for passwords).
  • Auth middleware extended with a PAT path: if the request carries Authorization: Bearer <token>, look up by hashed token; on hit, populate the request user the same way a session cookie does. Session cookies remain the only auth for browser routes; PATs are rejected there. PATs only authenticate API endpoints.
  • Scopes are minimal day-one: a single agent scope. The shape supports adding scopes later without a schema change.
  • Settings UI: "Personal access tokens" panel — list tokens (name, created_at, last_used_at, scope, expires_at), create new (with a name and optional expiry), revoke. Token plaintext is shown exactly once at creation.
  • Per-user scoping enforced: a PAT for user A authenticates as user A. Downstream queries are scoped the same as session-cookie traffic via the existing repository layer.
  • Cross-engine tests: token issue → use → revoke flow runs on both SQLite and Postgres.

Acceptance criteria

  • personal_access_tokens table migration applies on startup, both engines.
  • Creating a token from the settings UI returns the plaintext exactly once; a second view never shows the plaintext.
  • A request with a valid Authorization: Bearer <token> reaches authorised endpoints as that user; the same request with the token revoked returns 401.
  • A PAT for user A cannot read user B's data through any endpoint.
  • Tests run on both DB engines.

Part of epic #47. Builds on ADR-0004 (server-side sessions).

Add a per-user, revocable token primitive so external agent runtimes (Claude Code, opencode, custom MCP clients) can authenticate to Carol without holding a session cookie. Carol's current auth (ADR-0004) is server-side opaque sessions, browser-cookie only. That works perfectly for the PWA but doesn't translate to a CLI / desktop / headless client. A PAT system slots in alongside sessions: the auth middleware authorises a request whose `Authorization: Bearer <token>` header carries a valid PAT exactly as it would a valid session cookie, with the same per-user data isolation downstream. ## Scope - New `personal_access_tokens` table: `(id, user_id, token_hash, name, scopes, created_at, last_used_at, expires_at, revoked_at)`. Tokens are server-generated, returned to the user exactly once at creation, and stored hashed (argon2id, the same primitive we use for passwords). - Auth middleware extended with a PAT path: if the request carries `Authorization: Bearer <token>`, look up by hashed token; on hit, populate the request user the same way a session cookie does. Session cookies remain the only auth for browser routes; PATs are rejected there. PATs only authenticate API endpoints. - Scopes are minimal day-one: a single `agent` scope. The shape supports adding scopes later without a schema change. - Settings UI: "Personal access tokens" panel — list tokens (name, created_at, last_used_at, scope, expires_at), create new (with a name and optional expiry), revoke. Token plaintext is shown exactly once at creation. - Per-user scoping enforced: a PAT for user A authenticates *as* user A. Downstream queries are scoped the same as session-cookie traffic via the existing repository layer. - Cross-engine tests: token issue → use → revoke flow runs on both SQLite and Postgres. ## Acceptance criteria - [ ] `personal_access_tokens` table migration applies on startup, both engines. - [ ] Creating a token from the settings UI returns the plaintext exactly once; a second view never shows the plaintext. - [ ] A request with a valid `Authorization: Bearer <token>` reaches authorised endpoints as that user; the same request with the token revoked returns 401. - [ ] A PAT for user A cannot read user B's data through any endpoint. - [ ] Tests run on both DB engines. Part of epic #47. Builds on ADR-0004 (server-side sessions).
james closed this issue 2026-06-19 17:15:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#49
No description provided.