-
-
Notifications
You must be signed in to change notification settings - Fork 169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Concurrent token refreshes can lead to unintentional signing out of users when multiple tabs are open #213
Comments
@bnjmnt4n @inian I think this issue is not frontend related issue, I went deep into the gotrue server implementation, everytime you request a new refresh token, gotrue revokes the old one. As you are in different tabs with shared refresh tokens, the tab which does the refresh first revokes the refresh token for the other tabs, so the other tabs cant get a new access token. Overcoming this issue means that only one tab can make the refresh and it needs to share the access and refresh tokens with the others. Maybe the broadcast channell api or storage change event to share data between tabs? The second option is to not revoke the refresh token, but that would blow the security. From the GoTrue server source code, clearly visible that it revokes the old token. // GrantRefreshTokenSwap swaps a refresh token for a new one, revoking the provided token.
func GrantRefreshTokenSwap(tx *storage.Connection, user *User, token *RefreshToken) (*RefreshToken, error) {
var newToken *RefreshToken
err := tx.Transaction(func(rtx *storage.Connection) error {
var terr error
if terr = NewAuditLogEntry(tx, user.InstanceID, user, TokenRevokedAction, nil); terr != nil {
return errors.Wrap(terr, "error creating audit log entry")
}
token.Revoked = true
if terr = tx.UpdateOnly(token, "revoked"); terr != nil {
return terr
}
newToken, terr = createRefreshToken(rtx, user)
return terr
})
return newToken, err
} But if I missread somehow the go source code, please let me know. Edit: I checked also the gotrue-js implementation, they already listening for storage changes, when multitab support is enabled |
@TheOnlyBeardedBeast The gotrue behaviour is correct; this is more of an issue with the gotrue-js implementation.
This is exactly the gist of the problem. Both tabs do not notify each other that they are attempting a refresh, so it is possible for 2 parallel refresh attempts to occur at nearly the same time. One of them will likely succeed first, returning a new refresh token. However, the other refresh attempt will cause the new refresh token to be revoked due to the use of the same original refresh token. I think there are possibly 2 paths to take:
|
@bnjmnt4n thanks for the reply, I am working on a client myself handling the same issue (not gotrue-js, but the api was based on gotrue and behaves the same), currently what I am doing is listening to storage events and updating the clients refresh token memory representation in different tabs. It works well but I think in the future I can encounter similar issues as you when a new tab is opened on the edge of the refresh interval of the current tab so a refresh starts while a different refresh is in progress. Perhaps a simple onetime retry after the first failed refresh happens could also help, that failed refresh would mean that a new refresh token should already exist in the storage. But your idea with a locking mechanism sounds good. You could wrap a storage change event into a promise, checking for a specific key, before the refresh request you can await that given promise which resolves when the given key does not exist in the storage, or its value means that a refresh is not pending in a different tab. I will definitely try something out, thanks for the brainstorming. Edit: One more possible solution is to disable refresh for hidden tabs, and refresh them when they become activated. |
IMO this race condition can be resolved with leader election. That is, we select a single tab, using a leader election algorithm, to perform the token refresh when it is time to perform a refresh. There's a Alternatively, a temporary cheap solution would be to randomize the time at which the token refreshes. Though an incomplete solution, this would significantly reduce the likelihood of a clash. |
This is now being worked on server-side in gotrue: supabase/auth#466 |
@thorwebdev I see that even though supabase/auth#466 has been merged and deployed to my hosted project (I checked that my project is using v2.6.30 while the fix is in 2.6.28), the issue still reproduces. That is, if I have multiple tabs open, one of the refresh token calls fails and signs out the user from both tabs. Is there anything I need to do to enable the fix? |
@chipilov I'm not able to reproduce this anymore. Can you provide some screenshots of your network tabs? For me it is now correctly working and replaying the request (see screenshots). Do note that the default reuse interval is 10s. So if the requests are further apart than that the tokens will be revoked. |
Hi @thorwebdev, I am attaching a screenshot of the issue: Some additional info:
Finally, I am not sure what you mean by "replaying the requests". My impression was that the fix was entirely on the server side so that if 2 token refresh requests for the same token arrive at the server within 10s from each other both of them will receive 200 responses with the new token. Is that not the case (i.e. is there an additional retry handling in the client)? Let me know if you need more info. |
Hey @chipilov,
For example:
a. Token is used within reuse interval For example:
If a refresh request is made using |
Just to close the loop on my particular issue: @kangmingtay identified the problem (a newly introduced env var was not loaded in my project for some reason) and fixed it - thanks! So the server-side fix seems to work as long as the new env vars are properly loaded into a project. |
I can still reproduce this bug with local supabase instance. Supabase-cli: |
I run |
Bug report
Describe the bug
gotrue-js
useslocalStorage
to store the refresh token. When the user has multiple tabs open, multiple instances of theGoTrueClient
will be instantiated, with each reading fromlocalStorage
the same identical access and refresh tokens. There can be cases where both tabs will attempt to refresh using the same token at the same time, which leads to the first request succeeding, but the second request failing due to token reuse detection (supabase/auth#226).To Reproduce
Expected behavior
There is some kind of locking mechanism which avoids concurrent refreshes using the same refresh token. #203 attempts to do this, but it doesn't account for the multi-tab use case.
System information
This is likely reproducible on other browsers, but here's the one I tested with:
Additional context
I will be testing out some changes to
gotrue-js
and will send a PR if it successfully fixes this issue.The text was updated successfully, but these errors were encountered: