Run the full T2 §5 prose-vs-Workflow /trekreview bake-off (operator GO, choice "a"): 3 runs/arm on a rich-finding JWT-auth fixture, resolving the smoke's 0-finding limitation. Deliverables: - tests/fixtures/bakeoff-rich/ — JWT-auth brief + diff with 5 seeded blatant, brief-traceable issues (varied severity/rule_key, one dual-flaggable). - scripts/bakeoff-armA-merge.mjs — Arm A (prose) validate (NW1) + triplet-dedup, matching Arm B's dedup exactly. - scripts/bakeoff-fidelity.mjs — cross-arm + within-arm + granularity-ladder fidelity analysis over the structured arm outputs. - docs/T2-bakeoff-results.md §Full run — the T2 §5 verdict. Result (3 runs/arm, both arms ran the coordinator): - Verdict fidelity EQUIVALENT — all 6 runs BLOCK, cross-arm verdict-match 1.0. - Finding-set: substrate is fidelity-neutral. Cross-arm jaccard 0.41 (triplet) → 0.71 (file,rule_key) → 1.0 (file); cross-arm ≈ within-arm at every granularity. Issue coverage 5/5 in 6/6 runs. Low triplet jaccard is line-citation noise shared by both arms, not a substrate effect. - Token +4.4% (Arm B vs A; <=+15%). Classifier interference 0 at 9-agent concurrency. JSON-robustness: Arm B schema-forced; Arm A 6/6 valid via NW1. - VERDICT POSITIVE → S11 proceeds with opt-in --workflow flag. Caveat (per plan posture): strict triplet-jaccard>=0.7 flag is 0/9, a metric-calibration artifact (both arms sub-0.7 against themselves), not a regression. Residual: F4 auto/bypass explicit-mode check (mode not settable in-session). Suite green (662/660 pass, 2 skip); plugin validate clean (modulo the pre-existing root-CLAUDE.md warning). No production code changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
69 lines
2.7 KiB
Markdown
69 lines
2.7 KiB
Markdown
---
|
|
type: trekbrief
|
|
brief_version: "2.1"
|
|
slug: jwt-auth-refresh-rotation
|
|
task: Add JWT authentication with refresh-token rotation to the API
|
|
research_topics: 0
|
|
research_status: complete
|
|
brief_quality: ready
|
|
created: 2026-06-18
|
|
---
|
|
|
|
# JWT authentication with refresh-token rotation
|
|
|
|
## Intent
|
|
|
|
Add stateless JWT authentication to the API: a `/login` endpoint that issues a
|
|
short-lived access token plus a rotating refresh token, and a `/refresh`
|
|
endpoint that rotates the refresh token on every use. Tokens are signed with a
|
|
fixed RS256 key pair. This is the security boundary of the service, so the
|
|
contract below is strict.
|
|
|
|
## Plan reference (what the approved plan said to build)
|
|
|
|
> **Plan Step 4** — `lib/handlers/login.mjs` verifies the password with
|
|
> `bcrypt.compare(password, user.passwordHash)`. The stored credential is a
|
|
> bcrypt hash; no plaintext comparison.
|
|
>
|
|
> **Plan Step 6** — `lib/auth/jwt.mjs` hard-codes the verification algorithm to
|
|
> `['RS256']`. The algorithm is never read from the request.
|
|
|
|
## Success Criteria
|
|
|
|
- **SC1** — `POST /login` with valid credentials returns `200` with both an
|
|
`accessToken` and a `refreshToken` in the JSON body.
|
|
- **SC2** — `POST /login` with invalid credentials returns HTTP `401` (not 200)
|
|
and no tokens. Invalid means the email is unknown OR the password does not
|
|
match.
|
|
- **SC3** — Refresh-token rotation is covered by an automated test that
|
|
exercises the **concurrent-refresh** race window (two refreshes presenting the
|
|
same refresh token must not both succeed).
|
|
- **SC4** — Access and refresh tokens are signed and verified with **RS256
|
|
only**, using the server's fixed key pair.
|
|
|
|
## Non-Goals
|
|
|
|
- **NG1** — Do NOT accept a caller-supplied signing/verification algorithm. The
|
|
algorithm must never be read from the request (header, body, or query). A
|
|
token claiming a different `alg` must be rejected.
|
|
- **NG2** — Do NOT add a user-registration / sign-up endpoint. Users are
|
|
provisioned out of band.
|
|
- **NG3** — Do NOT add password-reset or email flows in this change.
|
|
|
|
## Constraints
|
|
|
|
- Node stdlib + the already-vendored `jsonwebtoken` and `bcrypt`; no new deps.
|
|
- Every delivered code path that the SCs describe must have test coverage.
|
|
- Errors from the refresh-token store (a network resource) must not crash the
|
|
request handler — degrade to a 5xx, do not let the rejection bubble unhandled.
|
|
|
|
## Assumptions
|
|
|
|
- `db.getUserByEmail(email)` returns `{ id, email, passwordHash }` or `null`.
|
|
- A `refreshStore` with `get/set/delete` (async, may throw on backend outage) is
|
|
injected.
|
|
|
|
## NFRs
|
|
|
|
- Constant-time password comparison via the bcrypt primitive (no hand-rolled
|
|
comparison over plaintext-derived buffers).
|