Skip to content

fix(auth): secure refresh-token handling and rebuild client refresh logic - #92

Merged
Alimedhat000 merged 9 commits into
developfrom
fix/refresh-token-storage
Aug 28, 2026
Merged

fix(auth): secure refresh-token handling and rebuild client refresh logic#92
Alimedhat000 merged 9 commits into
developfrom
fix/refresh-token-storage

Conversation

@Alimedhat000

Copy link
Copy Markdown
Owner

Closes #22

Summary

The refresh token was stored as an httpOnly cookie with SameSite=None; Secure unconditionally, which browsers drop in non-TLS dev environments, and the client's refresh logic was broken in several ways. This PR keeps the httpOnly-cookie transport (best practice, XSS-safe) per the issue discussion and fixes the surrounding behaviour.

Server

  • Logout now authenticates via the refresh cookie instead of a valid access token — previously an expired session (exactly when logout matters) could not log out (validateRefreshToken middleware already populates req.user).
  • Removed the unused accessToken cookie set on refresh; nothing ever read it.
  • Cookie flags are now environment-aware: SameSite=None; Secure only in production (cross-site HTTPS), Lax without Secure in dev/test where client and API are same-site over plain HTTP.

Client

  • New 401 response interceptor with single-flight refresh: on session expiry mid-use, it refreshes once via the cookie and replays the failed request. Concurrent 401s share one in-flight refresh, and a request whose token was already rotated replays directly without triggering a second round-trip.
  • Rebuilt bootstrap (AuthProvider): restores the session through the same shared refresh flight and marks the user signed out on failure — it no longer calls the authenticated /auth/logout endpoint on boot (guaranteed 401 + unhandled rejection).
  • Removed the wasLoggedOut localStorage hack, which permanently logged users out after any logout → re-login → reload cycle.

Tests

  • Server vitest: logout-via-cookie, no accessToken cookie, env-appropriate flags (60 passing).
  • New node-env vitest unit project for the client (--project unit, 5 tests) covering refresh-once-and-replay, single-flight under concurrency, failure notification, and no-refresh-when-signed-out.
  • E2E: logout → re-login → reload keeps the session; unauthenticated bootstrap never calls logout. Auth specs serialized since real logins overwrite the user's single stored refresh token. 33/33 passing against a live stack.

Notes

  • e2e/collaboration.spec.ts:103 fails on unmodified develop when E2E_API_URL isn't set to the local server port (it defaults to the CI port 5001); unrelated to this change.
  • Old clients may still carry the now-unset accessToken cookie; logout continues clearing it for migration safety.

Logout previously required a valid access token, so an expired session
could not log out (the exact moment logout matters). Reuse the
validateRefreshToken middleware, which already populates req.user for
the controller to revoke the stored token.
The refresh endpoint set an httpOnly accessToken cookie that no client
ever reads; the access token already travels in the response body.
SameSite=None + Secure is only valid for cross-site HTTPS deployments;
in dev the client and API are same-site over plain http, where Secure
cookies get dropped and None requires TLS. Use Lax/insecure outside
production, None/Secure in production.
Add a response interceptor that refreshes the access token once and
replays the failed request. Concurrent 401s share one in-flight refresh;
a request whose token was already rotated by a concurrent request
replays directly instead of triggering a second round-trip. When the
refresh itself fails, the stored token is cleared and listeners are
notified so the auth provider can reset state.

Also add a node-env vitest unit project so shared client lib code has a
test runner (the existing vitest setup only ran Storybook tests).
Bootstrap now restores the session via the shared single-flight refresh
and simply marks the user signed out when it fails — it no longer calls
the authenticated logout endpoint (which always 401'd and threw an
unhandled rejection). Remove the wasLoggedOut localStorage flag that
kept logging users out after a logout->login cycle, swallow logout
errors when the session is already dead server-side, and reset auth
state when the interceptor reports an unrecoverable session expiry.

Add e2e coverage for logout->re-login->reload session restore and for
not calling logout during an unauthenticated bootstrap; serialize auth
specs since real logins overwrite the user's single stored refresh
token.
clearCookie must mirror the cookie's Secure/SameSite/Path/HttpOnly
attributes, otherwise browsers retain the prod SameSite=None; Secure
cookie after logout and subsequent refresh still succeeds.
Verifies that logout clears both refreshToken and legacy accessToken
cookies with the same Path/HttpOnly/SameSite attributes used on set
(otherwise prod SameSite=None; Secure cookies survive logout and refresh
still succeeds), and that replaying the old cookie after logout is
rejected. Would have failed before 4f8cdf5.
Concurrent 401s sharing one refreshPromise notified listeners per waiter
(3×) and could replay with Bearer null after clearAccessToken. Move
clear+notify into refreshPromise rejection (once) and guard the
current !== _tokenUsed branch to throw when current is null instead of
asserting non-null.
@Alimedhat000
Alimedhat000 merged commit 93e9b52 into develop Aug 28, 2026
2 checks passed
@Alimedhat000
Alimedhat000 deleted the fix/refresh-token-storage branch August 28, 2026 01:28
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