Skip to content

@logto/react: possible refresh-token rotation race right after sign-in callback (double exchange + invalid_grant) #1073

@wangsijie

Description

@wangsijie

Summary

In a React SPA using Logto OSS (@logto/react) with an API resource, we sometimes see a refresh-token exchange race immediately after sign-in callback.

When isAuthenticated becomes true, if we immediately call:

await getAccessToken(LOGTO_RESOURCE)

the audit logs show:

  1. Exchange token by Refresh Token (success)
  2. RevokeToken
  3. Exchange token by Refresh Token (400 invalid_grant)

If we add a short delay (for example await new Promise((r) => setTimeout(r, 1000))) before the first getAccessToken(LOGTO_RESOURCE), the issue disappears (only one exchange, no revoke).

Environment

  • Deployment: Logto OSS
  • SDK: @logto/react (with API resource token flow)
  • App type: React SPA

Reproduction pattern

  1. User signs in and returns to callback route.
  2. Callback is handled by useHandleSignInCallback.
  3. As soon as app state sees isAuthenticated === true, app immediately requests resource token via getAccessToken(LOGTO_RESOURCE) and stores it for API calls.
  4. In this timing window, refresh token exchange can happen twice, second request fails with invalid_grant.

Expected behavior

  • First resource token fetch after callback should be race-safe.
  • SDK should avoid concurrent refresh-token exchanges that reuse the same refresh token.

Actual behavior

  • Two refresh-token exchanges can be triggered almost back-to-back.
  • First succeeds; second fails with invalid_grant.
  • RevokeToken appears in logs although app does not explicitly call token revocation endpoint.

Notes

From code reading:

  • isAuthenticated is set after handleSignInCallback() resolves in React SDK.
  • getAccessToken() dedupe is memoized per client instance.

So this may happen when multiple near-simultaneous token requests are made from different call sites / instances right after callback.

Workaround

Add a short delay before the first getAccessToken(LOGTO_RESOURCE) after callback. This is not ideal but avoids the race in our app.

Questions

  1. Is this a known refresh-token-rotation race in SPA usage?
  2. Is there a recommended SDK-level pattern/hook to wait until callback handling is fully settled before requesting resource tokens?
  3. Would it make sense to add stronger cross-call-site protection for first refresh-token exchange after callback?

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions