fix(auth): Log out on rejected refresh token instead of stalling startup#2812
fix(auth): Log out on rejected refresh token instead of stalling startup#2812charlesvien wants to merge 1 commit into
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
a883f15 to
5c85f4e
Compare
dd8572f to
3c2de4a
Compare
There was a problem hiding this comment.
The bot raised a valid unresolved concern: treating all HTTP 400s as auth errors is too broad — RFC 6749 returns 400 for invalid_client and invalid_request too, which should not trigger logout. The fix should check the error field in the response body for invalid_grant specifically, not blanket all 400s.
5c85f4e to
de89b91
Compare
There was a problem hiding this comment.
The auth deny-list gate blocked this PR. While the code correctly narrows 400 handling to only invalid_grant/invalid_token (addressing the bot reviewer's concern), auth/token refresh logic requires a human security reviewer to approve — automated gates are configured to require it here.

Problem
When a stored session holds a refresh token the server rejects (you logged in against a dev instance, or the token expired or was revoked), the desktop app hung on a blank "Loading..." screen and, 25 seconds later, showed "PostHog is taking longer than expected to start" with Retry and Get support. Restarting did not help, because the dead token was retried on every launch.
Root cause: the OAuth2 token endpoint returns an invalid, expired or revoked refresh token as HTTP 400 (
invalid_grant, RFC 6749 section 5.2). Our OAuth client only classified 401/403 as auth errors, so a 400 fell through tounknown_error. That code is non-retryable and does not clear the stored session, soAuthServicedropped bootstrap back into therestoringstate withbootstrapComplete: falseand never recovered. The UI sat on the loading screen until the 25s stall fallback fired.Console from a failing launch:
Changes
packages/core/src/oauth/oauth.ts: classify HTTP 400 as anauth_erroralongside 401/403. A rejected refresh token now takes the existing forced-logout path inAuthService.refreshSession: it clears the stored session, publishes anonymous state (bootstrapComplete: true) and routes the user straight to the login screen. No retries, no spinner, no stall screen.packages/core/src/oauth/oauth.test.ts: add a case asserting 400 is classified asauth_error, and repoint the other-4xx case to 404.Unchanged: 5xx is still retried (
server_error), fetch failures staynetwork_errorand other 4xx stayunknown_error. The bootstrap stall fallback is kept as a safety net for a genuinely slow boot; it just no longer triggers for auth failures.How did you test this?
pnpm --filter @posthog/core test: full core suite, 1616 passed. Covers both layers of the fix:oauth.test.tsproves a 400 is classified asauth_error, andauth.test.ts("does not retry on auth_error and forces logout") proves that anauth_errorclears the session and returns anonymous state.pnpm --filter @posthog/core typecheck: clean.biome linton the two changed files: clean.pnpm typecheckand Biome on commit: passed.Not done: I did not reproduce the hang in the live Electron app. The coverage above is unit level across the classifier and the logout path.
Automatic notifications