Authentication
uwwoe’s authentication model is intentionally minimal: anonymous bearer tokens, no accounts, no OAuth, no API keys.
Tokens
A token is issued by POST /api/v1/state (the only endpoint that mints
one). It is:
- ≥256 bits of cryptographic randomness, base64url-encoded with no padding.
- Returned in the response body exactly once.
- Sent on subsequent requests as
Authorization: Bearer <token>.
The backend stores only an HMAC verifier under a rotatable key — not the token itself. (ADR 0011)
Required vs optional
| Endpoint family | Token required? |
|---|---|
GET /api/v1/health, /diagnostics, /index | No |
GET /api/v1/courses, /courses/{..}/{..}, /credentials, /credentials/{..}, /source-references/{..} | No |
POST /api/v1/query/course-unlock, course-impact, credential-progress, credential-gap-summary, what-if, advisory | Required when querying against persisted state; optional with state_mode: "supplied" |
POST /api/v1/graph/views/* | Required for state-overlay views (unlock-overlay, target-relevance); optional for stateless views |
POST /api/v1/state | No (mints the token) |
GET/PATCH/PUT/DELETE /api/v1/state/current, /state/current/export, /state/current/migration-preview | Required |
GET /internal/metrics | Internal operator bearer (UWSCRAPE_INTERNAL_TOKEN), separate from state tokens |
Header only
The token is only accepted via Authorization: Bearer <token>.
Authorization: Bearer <token>Tokens supplied as:
- Query parameters (
?token=...) → rejected. - URL fragments → ignored (browsers don’t send fragments to the server, but the backend would reject them anyway).
- Cookies → not used at all.
This is enforced server-side and asserted by test gates.
What never logs the token
- The runtime access log.
- The
/internal/metricsendpoint. - Atlas worker payloads.
- Shareable view URLs.
- Test artifacts and CI logs.
Gate 4 of every phase verifies this.
Errors
| Status | Code | Cause |
|---|---|---|
401 | missing_token | A token-required endpoint was called without Authorization. |
401 | unauthorized | Token did not match any stored verifier (revoked, hard-deleted, or never existed). |
403 | token_in_query | A token was supplied via query parameter — rejected by policy. |
A 401 unauthorized after a hard delete is indistinguishable from a
401 unauthorized for any never-existing token. This is by design
(ADR 0019).
Lifecycle
- Mint —
POST /api/v1/state→ receive token once. - Use — every subsequent state-bearing call carries
Authorization: Bearer <token>. - Lose — token gone, plan unrecoverable (no identity to recover against).
- Delete —
DELETE /api/v1/state/currentwith explicit confirmation → hard delete, tombstone, token retired.
Want more?
- Your plan — user-facing view of the same model.
- State and privacy — full posture.
- Decision: Anonymous state token policy.