-
-
Notifications
You must be signed in to change notification settings - Fork 60
@logto/react: possible refresh-token rotation race right after sign-in callback (double exchange + invalid_grant) #1073
Copy link
Copy link
Open
Labels
Description
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:
Exchange token by Refresh Token(success)RevokeTokenExchange token by Refresh Token(400invalid_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
- User signs in and returns to callback route.
- Callback is handled by
useHandleSignInCallback. - As soon as app state sees
isAuthenticated === true, app immediately requests resource token viagetAccessToken(LOGTO_RESOURCE)and stores it for API calls. - 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. RevokeTokenappears in logs although app does not explicitly call token revocation endpoint.
Notes
From code reading:
isAuthenticatedis set afterhandleSignInCallback()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
- Is this a known refresh-token-rotation race in SPA usage?
- Is there a recommended SDK-level pattern/hook to wait until callback handling is fully settled before requesting resource tokens?
- Would it make sense to add stronger cross-call-site protection for first refresh-token exchange after callback?
Reactions are currently unavailable