feat(integration): scoped bearer API tokens + JSON API v1 (LC-72) #167
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-72-api-tokens"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Scoped personal API tokens + a documented JSON API (
/api/v1) so bots / scripts / CI can call lets-chat with a bearer token, scoped narrower than the owning user (LC-72). The cookie session path is unchanged; token auth is additive.Design
0019,api_tokens): only an HMAC-SHA256 of the token (keyed byLETS_CHAT_SECRET_KEY) is stored; plaintext is shown once at creation, never persisted or logged. Space-separated scopes, optional expiry,revoked_at. Revoked/expired rows retained for audit.hmac+hexmoved from saas-only to unconditional deps.ApiAuthextractor: readsAuthorization: Bearer, HMACs, resolves User + scope set. Missing/unknown/expired/revoked token (or no server secret) -> 401.last_used_atbumped in a detached task (never blocks).ApiAuth::require(scope)-> 403 when a valid token lacks the scope (so undeclared-scope routes are unreachable by tokens)./api/v1(JSON): merged after the cookie/2FA/maintenance/branding layers, so it bypasses the browser session.GET /me(any token),GET /rooms(rooms:read),GET /rooms/{id}/messages(messages:read),POST /rooms/{id}/messages(messages:write). Every route still enforces the user's own room access + ban/mute.docs/api.mdlists every API route + scope.Acceptance criteria
last_used_atupdates without blocking the request.Open questions left for later: admin-mint-on-behalf, per-token rate limits, IP allow-lists, LC-73 bot interaction.
Tests
routes_api.rs: no/bad token 401, identity, missing-scope 403, write+read round-trip, expiry 401, immediate-revoke 401. Migration0019appended to hand-rolled auth lists.just check,just test,just test-saasgreen.🤖 Generated with Claude Code
A documented HTTP API external programs can call with a personal bearer token, scoped narrower than the owning user. Tokens (migration 0019, auth.db api_tokens): only an HMAC-SHA256 of the token (keyed by the server secret) is stored; the plaintext is shown exactly once at creation and never persisted or logged. Rows carry a space-separated scope list, optional expiry, and revoked_at; revoked/expired rows are retained for audit. hmac + hex moved from saas-only to unconditional deps so the HMAC is available in every build. Auth: new ApiAuth extractor (FromRequestParts<AppState>) reads Authorization: Bearer, HMACs, looks up the token, and resolves the User + scope set. Missing / unknown / expired / revoked token (or no server secret) -> 401. last_used_at is bumped in a detached task so it never blocks the request. Scope enforcement is per-route via ApiAuth::require(scope), which returns 403 (never 401) when a valid token lacks the scope - so a route that declares no scope is unreachable by tokens. API surface (/api/v1, JSON): merged AFTER the cookie / 2FA / maintenance / branding middleware so it bypasses the browser session entirely. GET /me (any valid token), GET /rooms (rooms:read), GET /rooms/{id}/messages (messages:read), POST /rooms/{id}/messages (messages:write). Every route still enforces the token owner's own room access + ban/mute - scopes narrow, never widen. Management UI: Settings -> API tokens (GET/POST /settings/api-tokens, POST .../revoke). Mint with a name + scope checkboxes + optional expiry; the plaintext is rendered once on the create response. Available in both standalone and saas. docs/api.md is the handwritten reference listing every API route and its scope. Tests: routes_api.rs covers no/bad token 401, valid identity, missing-scope 403 (not 401), write+read message round-trip, expiry 401, and immediate-revoke 401. Migration 0019 appended to the hand-rolled auth migration lists. just check, just test, just test-saas all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>