--- 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).