---
type: ultrareview
review_version: "1.0"
created: 2026-05-01
task: "Add JWT authentication with refresh-token rotation"
slug: jwt-auth
project_dir: .claude/projects/2026-05-01-jwt-auth/
brief_path: .claude/projects/2026-05-01-jwt-auth/brief.md
scope_sha_start: 0123456789abcdef0123456789abcdef01234567
scope_sha_end: fedcba9876543210fedcba9876543210fedcba98
reviewed_files_count: 3
verdict: WARN
findings:
  - d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232
  - 7861519c326c207aabf17072db51c469bebc217b
  - 400dfcff81e0e219eb04a7123c68ae870696f121
  - 763d174e6c519fafbadcba5d1706708479e36e61
  - 7a3d7d0a668f6431ef3877ceeb106023b0f6295e
---

# Review: Add JWT authentication with refresh-token rotation (Run A)

## Executive Summary

Implementation hits the brief's core success criteria (login + refresh + logout) but
has one BLOCKER and four MAJOR/MINOR issues. Verdict: **WARN** — fix the BLOCKER
before merge; the MAJORs should land in a follow-up plan.

This is a SYNTHETIC v1.0 fixture for testing the Jaccard determinism pipeline. It is
NOT the output of a real LLM review.

## Coverage

| File | Treatment | Reason |
|---|---|---|
| `lib/auth/jwt.mjs` | deep-review | Security-critical (token signing/verification) |
| `lib/handlers/login.mjs` | deep-review | Auth surface |
| `lib/handlers/logout.mjs` | deep-review | Auth surface |
| `package-lock.json` | skip | Lockfile |
| `dist/**` | skip | Build output |

## Findings (BLOCKER)

### 763d174e6c519fafbadcba5d1706708479e36e61

- **Location:** `lib/handlers/login.mjs:23`
- **Rule:** `UNIMPLEMENTED_CRITERION`
- **Brief ref:** SC-2 ("login endpoint MUST return 401 on invalid credentials")
- **Evidence:** Handler returns 200 with empty body when password mismatch occurs.
- **Fix:** Return 401 with WWW-Authenticate header per brief SC-2.

## Findings (MAJOR)

### d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232

- **Location:** `lib/auth/jwt.mjs:42`
- **Rule:** `SECURITY_INJECTION`
- **Brief ref:** Non-Goal #3 ("must not accept user-supplied algorithm header")
- **Evidence:** `jwt.verify(token, secret, { algorithms: req.body.alg })` — algorithm taken from request body.
- **Fix:** Hard-code `algorithms: ['RS256']`; reject any token claiming a different alg.

### 7861519c326c207aabf17072db51c469bebc217b

- **Location:** `lib/auth/jwt.mjs:88`
- **Rule:** `MISSING_TEST`
- **Brief ref:** SC-4 ("refresh-token rotation must be tested under concurrent refresh")
- **Evidence:** No test in `tests/` covers the concurrent-refresh path; only happy-path is exercised.
- **Fix:** Add `tests/auth/concurrent-refresh.test.mjs` covering the race window.

### 7a3d7d0a668f6431ef3877ceeb106023b0f6295e

- **Location:** `lib/handlers/login.mjs:56`
- **Rule:** `PLAN_EXECUTE_DRIFT`
- **Brief ref:** Plan Step 4 ("login.mjs uses bcrypt.compare()")
- **Evidence:** Plan said `bcrypt.compare`; implementation uses `crypto.timingSafeEqual` over plaintext-derived buffers.
- **Fix:** Either update plan + brief to record the deviation or refactor to bcrypt.compare per plan.

## Findings (MINOR)

### 400dfcff81e0e219eb04a7123c68ae870696f121

- **Location:** `lib/auth/jwt.mjs:117`
- **Rule:** `MISSING_ERROR_HANDLING`
- **Brief ref:** none (engineering hygiene)
- **Evidence:** `await refreshTokenStore.delete(jti)` is not wrapped — store-down throws bubble to top-level handler.
- **Fix:** Wrap in try/catch; log + 503 on store failure.

## Remediation Summary

5 findings total: 1 BLOCKER, 3 MAJOR, 1 MINOR. Run a remediation plan via
`/ultraplan-local --brief review.md` — it will pick up BLOCKER + MAJOR findings as
plan goals and emit `source_findings: [<id>, ...]` audit trail (Handover 6).

```json
{
  "fixture_kind": "synthetic-v1.0",
  "jaccard_with_run_B": "5/6 = 0.833",
  "findings": [
    {"id": "763d174e6c519fafbadcba5d1706708479e36e61", "severity": "BLOCKER", "rule": "UNIMPLEMENTED_CRITERION", "file": "lib/handlers/login.mjs", "line": 23},
    {"id": "d2d0e27875ae9ef0d818cb08bb6f14e6d33c4232", "severity": "MAJOR", "rule": "SECURITY_INJECTION", "file": "lib/auth/jwt.mjs", "line": 42},
    {"id": "7861519c326c207aabf17072db51c469bebc217b", "severity": "MAJOR", "rule": "MISSING_TEST", "file": "lib/auth/jwt.mjs", "line": 88},
    {"id": "7a3d7d0a668f6431ef3877ceeb106023b0f6295e", "severity": "MAJOR", "rule": "PLAN_EXECUTE_DRIFT", "file": "lib/handlers/login.mjs", "line": 56},
    {"id": "400dfcff81e0e219eb04a7123c68ae870696f121", "severity": "MINOR", "rule": "MISSING_ERROR_HANDLING", "file": "lib/auth/jwt.mjs", "line": 117}
  ]
}
```
