fix(session): use auth context refresh token instead of stale request cookie#55
fix(session): use auth context refresh token instead of stale request cookie#55
Conversation
… cookie When the middleware's withAuth() auto-refreshes an expired access token, the old refresh token in the original request cookie is invalidated by WorkOS. However, getSessionWithRefreshToken() was re-reading the session from the original request, getting the now-invalid old refresh token. Subsequent operations like switchToOrganization would then fail with "Failed to refresh tokens" because the stale token was rejected by the WorkOS API. The fix reads the refresh token directly from the middleware auth context (AuthResult), which always has the latest token — whether the middleware refreshed or not. This eliminates the race condition entirely. Fixes #53
Greptile SummaryThis PR fixes a token invalidation race condition in Key changes:
Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant Middleware as withAuth() Middleware
participant AuthContext as Auth Context (AuthResult)
participant WorkOS
Client->>Middleware: Request with expired access token (cookie)
Middleware->>WorkOS: Refresh tokens (old refresh token)
WorkOS-->>Middleware: New access token + new refresh token
Middleware->>AuthContext: Store refreshed AuthResult (new tokens)
Note over Middleware,AuthContext: OLD behaviour (broken)
Client->>getSessionWithRefreshToken: Call
getSessionWithRefreshToken->>Middleware: getSession(ctx.request) — reads cookie
Middleware-->>getSessionWithRefreshToken: OLD (stale) refresh token
getSessionWithRefreshToken->>WorkOS: switchToOrganization / refreshAuth
WorkOS-->>getSessionWithRefreshToken: ❌ "Failed to refresh tokens"
Note over AuthContext,WorkOS: NEW behaviour (fixed)
Client->>getSessionWithRefreshToken: Call
getSessionWithRefreshToken->>AuthContext: auth.refreshToken
AuthContext-->>getSessionWithRefreshToken: NEW (valid) refresh token
getSessionWithRefreshToken->>WorkOS: switchToOrganization / refreshAuth
WorkOS-->>getSessionWithRefreshToken: ✅ Success
Last reviewed commit: 73a7396 |
| * even if the middleware auto-refreshed during withAuth(). | ||
| * Returns null if no valid session exists. | ||
| */ | ||
| export async function getSessionWithRefreshToken(): Promise<{ |
There was a problem hiding this comment.
Redundant async keyword
After this refactor, getSessionWithRefreshToken no longer contains any await expressions. The async keyword is now redundant — the function still behaves correctly (returning a resolved Promise), but linters with @typescript-eslint/require-await will flag this, and it may be slightly misleading to future readers who expect an I/O operation inside.
Consider removing async and returning Promise.resolve(...) explicitly, or alternatively converting the return type from Promise<...> to a synchronous return and updating all call sites (though since the public API uses await, keeping the Promise wrapper is safest).
Manual Verification with
|
Summary
switchToOrganization(andrefreshAuth) failing with "Failed to refresh tokens" when the middleware auto-refreshes an expired access tokengetSessionWithRefreshToken()was re-reading the session from the original request cookie, which has a stale refresh token after middleware refresh. Now reads from the auth context instead, which always has the latest token.Root cause
When a user's access token expires, the middleware's
withAuth()automatically refreshes it — which invalidates the old refresh token at WorkOS. ButgetSessionWithRefreshToken()was callingauthkit.getSession(ctx.request)to read the session from the original request, getting the now-invalid old refresh token. Any subsequent operation using that token (switchToOrganization,refreshAuth) would fail because WorkOS rejects the stale token.The auth context (
AuthResult) from the middleware already contains the correct (refreshed) refresh token. The fix simply reads it from there instead of re-reading the original request.What changed
src/server/auth-helpers.ts:getSessionWithRefreshToken()now readsauth.refreshTokenfrom the middleware context instead of re-reading the session from the request cookiesrc/server/auth-helpers.spec.ts: Updated tests + added test for the middleware-refresh race conditionTest plan
WORKOS_COOKIE_NAME=my-custom-session(see screenshots below)Manual verification
Tested with Playwright against the example app with
WORKOS_COOKIE_NAME=my-custom-sessionset in.env.Authenticated with custom cookie name —
/clientpageAfter clicking Switch — org claims populated successfully
Steps:
WORKOS_COOKIE_NAME=my-custom-sessionmy-custom-sessioncookie in browser/client, entered org ID, clicked Switchmember), Roles populated. Green callout shown. No errors.Fixes #53