Skip to content

fix(auth-next-server): stop resending a rejected refresh token - #2960

Draft
brayansdt wants to merge 1 commit into
mainfrom
brayansdt/auth-next-server-terminal-refresh
Draft

brayansdt wants to merge 1 commit into
mainfrom
brayansdt/auth-next-server-terminal-refresh

Conversation

@brayansdt

Copy link
Copy Markdown
Contributor

Summary

auth-next-server no longer resends a refresh token that the auth server has rejected with invalid_grant. Before this change, a session whose refresh token was rejected tried the same token again on every session read, for as long as the session cookie lived.

Detail and impact of the change

Fixed

  • When a refresh fails, the jwt callback set error: 'RefreshTokenError' but kept the refresh token, and the access token stayed expired. Every later session read (each /api/auth/session poll from useSession, each auth() call) tried the same refresh again. The session cookie lasts 365 days and is renewed on each read, so a single stuck session keeps calling the token endpoint indefinitely. In practice that means a steady stream of failed refreshes, with bursts of thousands a minute when one client polls quickly.
  • Now, when the token endpoint answers invalid_grant, the refresh token is removed from the JWT. Later reads take the existing "no refresh token" branch, with no network call, and the session keeps reporting RefreshTokenError.
  • The error stays RefreshTokenError rather than falling back to TokenExpired, so apps that sign the user out on RefreshTokenError still do. TokenExpired is still returned for sessions that never had a refresh token.
  • Only invalid_grant counts as terminal. Auth0 returns it with a 403 and Ory with a 400, so the OAuth error code is checked, not the HTTP status. Network errors, 5xx, 429 and other error codes keep the refresh token, and the next read retries as before.
  • The forceRefresh (trigger: 'update') path gets the same handling.

Added

  • RefreshTokenRequestError, thrown by refreshAccessToken when the token endpoint rejects the request. It carries status and oauthError, and still extends Error with the same message as before.
  • isTerminalRefreshError(error).
  • The first tests for auth-next-server: refresh.test.ts and config.test.ts. 7 of the 10 fail on main.

Anything else worth calling out?

  • Sessions that are stuck today clean themselves up after upgrading: the next read makes one failed refresh, the token is dropped, and nothing more is sent.
  • fix(auth-next-server): deduplicate concurrent token refresh to prevent invalid_grant #2865 (dedupe concurrent refreshes) is complementary: it reduces how often invalid_grant happens, and this PR stops the retries once it has. Both touch refresh.ts, so whichever merges second needs a small rebase.
  • oxlint reports one warning in config.ts (an unused idToken in jwt.encode). It is already on main.
  • Tested with pnpm --filter @imtbl/auth-next-server test (10/10) and typecheck (clean).

🤖 Generated with Claude Code

When the token endpoint rejects a refresh token with `invalid_grant`, the
jwt callback kept the token and only set `error: 'RefreshTokenError'`.
The access token stays expired, so every later session read (each
`/api/auth/session` poll, each `auth()` call) sent the same dead token
again, for the life of the session cookie (365 days, renewed on each read).

Now a terminal failure (`invalid_grant`, which Auth0 returns with 403 and
Ory with 400) drops the refresh token. Later reads take the
no-refresh-token branch without a network call and keep reporting
`RefreshTokenError`, so apps that sign out on it still do. Network errors,
5xx and 429 keep the token so the next read can retry.

`refreshAccessToken` now throws `RefreshTokenRequestError` (status +
OAuth error code); it and `isTerminalRefreshError` are exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nx-cloud

nx-cloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 3c9fc92

Command Status Duration Result
nx affected -t build,test ✅ Succeeded 37s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-11-04 23:49:50 UTC

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant