Skip to content

Refresh access tokens in proxy instead of bouncing users to login#127

Merged
eswan18 merged 2 commits into
mainfrom
fix/token-refresh-in-proxy
Apr 4, 2026
Merged

Refresh access tokens in proxy instead of bouncing users to login#127
eswan18 merged 2 commits into
mainfrom
fix/token-refresh-in-proxy

Conversation

@eswan18
Copy link
Copy Markdown
Owner

@eswan18 eswan18 commented Apr 4, 2026

Summary

  • The OAuth callback sets a 30-day refresh_token cookie, but refreshAccessToken() in lib/idp/client.ts had zero call sites — so when the short-lived access token expired, users were redirected to the IDP login page even though a valid refresh token was sitting in their cookies
  • The proxy now decodes the access token's exp claim, attempts a refresh via the refresh_token cookie when within 60s of expiry, and updates both cookies (preserving refresh token rotation if the IDP returns a new one)
  • If refresh fails, the stale access token is cleared and the user is redirected to /login as before
  • Effective session lifetime goes from ~1 hour (access token TTL) to 30 days, sliding if the IDP rotates refresh tokens

Test plan

  • npm run test — 183 passing, including 19 new tests
  • npm run build — Edge runtime bundle compiles cleanly
  • Local smoke test: log in, delete token cookie in DevTools, reload — confirm no login redirect and new token cookie appears
  • Refresh-failure path: set refresh_token cookie to garbage, reload — confirm redirect to /login (not a loop)
  • Long-session test: leave tab idle past access-token expires_in, reload — confirm no login redirect

🤖 Generated with Claude Code

eswan18 and others added 2 commits April 4, 2026 11:17
The OAuth callback already sets a 30-day refresh_token cookie, but
refreshAccessToken() in lib/idp/client.ts had no call sites — so when
the short-lived access token expired, users were redirected to the
IDP login page even though a valid refresh token was sitting in their
cookies.

The proxy now decodes the access token's exp claim, attempts a refresh
via the refresh_token cookie when the access token is within 60s of
expiry, and updates both cookies (preserving refresh token rotation if
the IDP returns a new one). If refresh fails, the stale access token
is cleared and the user is redirected to /login as before.

Effective session lifetime goes from ~1 hour (access token TTL) to
30 days, sliding if the IDP rotates refresh tokens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Forward the new access token to downstream handlers on the refresh
  request itself via request.cookies.set + NextResponse.next({ request }).
  Without this, Server Components rendered during the refreshing request
  read the stale cookie and render as logged-out for one page load, even
  though the browser gets the new cookie on the next request.
- Clear the refresh_token cookie on refresh failure. Leaving it set meant
  every subsequent navigation re-triggered the same failing IDP round-trip.
- Validate that the refresh response actually contains a non-empty
  access_token and expires_in; treat missing/empty as a refresh failure
  rather than silently setting broken cookies.
- Log refresh failures via the existing logger.warn so IDP outages are
  distinguishable from normal token expirations.

Also expand test coverage: assert cookie security attributes (httpOnly,
sameSite, path), verify the home-page redirect branch omits the redirect
query param, cover string/null exp claims in decodeJwtExp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@eswan18 eswan18 merged commit 0ce394d into main Apr 4, 2026
1 check passed
@eswan18 eswan18 deleted the fix/token-refresh-in-proxy branch April 4, 2026 16:37
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