Skip to content

Release v1.3.2#44

Merged
revtex merged 5 commits intomainfrom
dev
Apr 29, 2026
Merged

Release v1.3.2#44
revtex merged 5 commits intomainfrom
dev

Conversation

@revtex
Copy link
Copy Markdown
Owner

@revtex revtex commented Apr 29, 2026

Cuts v1.3.2.

Primary fix: refresh-token cookie path widened from /api/auth to /api so the browser actually sends it to /api/v1/auth/refresh — the root cause of the ~15-minute logoffs reported against 1.3.0 and 1.3.1.

Also includes:

  • Audio-element auth recovery shares the single-flighted refresh promise.
  • Backend grace window per OAuth 2.0 Security BCP §4.13 (30s tolerance for duplicate refresh-token presentations).
  • Regression tests that fail loudly if the cookie path stops covering every refresh endpoint.

See CHANGELOG.md for full details.

OpenScanner Dev added 5 commits April 29, 2026 17:18
…aces

Frontend (single-flight, all paths):
- Audio-element auth recovery now goes through the shared refreshSession()
  promise instead of issuing its own POST /api/v1/auth/refresh. The 1.3.1
  RTK Query single-flight did not cover this path, so a 401 on an <audio>
  fetch racing the scheduled refresh would replay the single-use cookie
  and revoke the token family (~25-min logoffs + random logoffs near
  audio playback errors).

Backend (OAuth 2.0 Security BCP \u00a74.13 grace window):
- PostRefresh now caches the successor (access JWT + raw refresh cookie
  + user identity) keyed by the old token hash on every successful
  rotation, with a 30s TTL.
- A second presentation of the same already-rotated cookie within the
  grace window returns the cached successor verbatim instead of revoking
  the family. This absorbs scenarios the per-tab single-flight cannot
  cover: parallel tabs, service-worker retries, reload mid-rotation,
  and any other harmless duplicate.
- Replays after the grace window (or with no cache entry — e.g. server
  restart between rotation and replay) still revoke the entire family,
  preserving the theft-detection signal.

Tests:
- New TestRefreshToken_ReplayWithinGrace_Idempotent: same cookie replayed
  twice returns identical access JWT + refresh cookie, family stays active.
- New TestRefreshToken_ReplayAfterGrace_RevokesFamily: cookie revoked
  out-of-band (simulating expired cache entry) still triggers family
  revocation on replay.
…eceives it

The refresh-token cookie was scoped to Path=/api/auth, but 1.3.0 moved the
frontend to /api/v1/auth/refresh. Per RFC 6265 \u00a75.1.4 path-matching,
/api/auth does NOT cover /api/v1/auth/refresh, so the browser silently
stopped sending the refresh cookie on every silent-refresh attempt. The
server returned 401 "no refresh token" the moment the 15-minute access
JWT expired and the user was bounced to the login screen.

This is the actual root cause of the ~15-minute logoffs that 1.3.1 and the
single-flight/grace-window fixes were chasing — the rotation logic was
never even reached. The defense-in-depth changes still stand for genuine
parallel-tab and SW-race scenarios that surface once the cookie is
delivered, but this one-line scope fix is what unbreaks the v1 surface.
The 1.3.0 cookie-path bug shipped because the existing tests only
checked that ck.Path equalled the auth.RefreshCookiePath constant —
a tautology that would have passed even if the constant were /cheese.
The handler-level refresh tests used httptest.NewRequest +
req.AddCookie(...), which force-attaches the cookie regardless of
RFC 6265 path-matching, so they couldn't have caught a path mismatch
either.

Two layers added:

1. cookie_test.go: assertCookiePathCovers walks every URL the cookie
   must reach (refresh + session endpoints, both legacy and v1) and
   fails if the cookie's Path is not a prefix per RFC 6265 §5.1.4.
   Adding a new endpoint? Append it to refreshEndpointPaths /
   sessionEndpointPaths.

2. refresh_test.go: TestRefreshCookie_DeliveredToEveryRefreshEndpoint
   uses a real http.Client + cookiejar.Jar against httptest.NewServer
   so the standard library enforces path-matching exactly the way a
   browser does. Login once, then POST every refresh URL the frontend
   can hit and assert 200. Reverting the path fix makes both layers
   fail loudly.
@revtex revtex merged commit cf8b003 into main Apr 29, 2026
13 checks passed
@revtex revtex deleted the dev branch April 29, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant