-
-
Notifications
You must be signed in to change notification settings - Fork 166
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
fix: refresh token race condition #274
Conversation
src/GoTrueClient.ts
Outdated
@@ -643,7 +644,7 @@ export default class GoTrueClient { | |||
const { currentSession, expiresAt } = data | |||
const timeNow = Math.round(Date.now() / 1000) | |||
|
|||
if (expiresAt < timeNow) { | |||
if (expiresAt < timeNow + 10) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be 60s to give you more buffer? However when you set JWT expiry to 30s for example it then refreshes constantly. @kangmingtay maybe we should enforce a minimum expiry of 120 seconds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed it to 60 seconds to be consistent with the setTimeout refreshDurationBeforeExpires
. However this means if your JWT expiry is below 60 seconds you're constantly calling gotrue. So we should probably enforce a minimum JWT expiry?
Awesome stuff!! Does change this handle the case where a tab is closed mid-refresh? A lock could be acquired and not released. |
@liaujianjie it does now, thanks for pointing it out! 💚 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the flow is still a bit more complex than it needs to be, lets discuss offline
@@ -1,8 +1,7905 @@ | |||
{ | |||
"name": "@supabase/gotrue-js", | |||
"version": "0.0.0", | |||
"lockfileVersion": 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this change Thor? Lock files should be v1 for now
@@ -634,7 +635,7 @@ export default class GoTrueClient { | |||
*/ | |||
private async _recoverAndRefresh() { | |||
try { | |||
const json = isBrowser() && (await this.localStorage.getItem(STORAGE_KEY)) | |||
const json = isBrowser() && (await this.localStorage?.getItem(STORAGE_KEY)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extract localstorage.(get, set, removeitem) into a utility cos we seem to be doing isbrowser() everytime before accessing it and will lead to errors if we dont do that guard. (can be in a separate PR)
@thorwebdev I just took a quick glance at the visibility handler. You probably do not want to automatically refresh on every visible event. You should do the expiration test for what ever time you agree to before renewing. That event fires for any tab change on a browser. Desktop/mobile does not matter. If the user tabs to check the stock market or news constantly, getting a new token will happen every time. EDIT: adding. Also I don't think you want to shut of refreshing when going hidden, I think that is what you are doing. That would be a change to existing behavior of apps being able to run the the background on many desktops. Token refresh still occurs currently. Also this would break realtime in desktop background tabs. I keep realtime alive for 3 minutes when a tab goes to the background with a timer so people can tab around a bit without needed to reload from the database all the monitored data. The 3 minute limit is because of this: supabase/realtime-js#121 |
If you are attempting to fix gotrue.js token refresh in general (versus just token race condition), I believe this issue of token refresh fails if the device is offline #226 (they also have a PR that seems overly complex to me) might need be addressed as well. I've not researched the online event handler. Also wondering what happens if the server/internet connection is down right at moment of token refresh timer going off and then comes back. |
src/GoTrueClient.ts
Outdated
this.localStorage?.removeItem(`${STORAGE_KEY}-tokenRefreshLock`) | ||
this._recoverAndRefresh() | ||
}, | ||
5 * 1000 // remove lock after 5s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it help if we randomise this setTimeout
duration too? in the event there are multiple tabs waiting for the lock, there could be multiple instances of setTimeout
invoked at the same time?
maybe it's better if the original lock setter calls setTimeout
to remove the lock after 5 seconds after it acquires the lock here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: I see the timer is for a release of the lock if the "other" tabs does not unlock, which covers below. Sorry for the interruption...
On the lock with a timer... What happens if the tab that set the lock gets closed right after that?
I'll admit I'm not up to speed on the need for multiple tabs and what the user can do with these tabs, but they can certainly close one at any point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok i just tested out the following and closed the tab before the timeout expired:
setTimeout(function(){
console.log("Hello from setTimeout")
}, 5000);
seems like once you close the tab, the timeout gets cleared so yeah it needs to be called by another tab and not the original lock setter
I switched course and decided to not use the lock but rather rely on a combination of a) random timeout offset: https://github.com/supabase/gotrue-js/pull/274/files#diff-3522461172efd6058d6b8da62fc2d30d8b524d2b64894ea2c67218c52f7fdff5R716 It does limit the chance of the race condition happening, but it does feel a bit hacky... Would appreciate your thoughts! |
Upfront I'll say I'm lost on where this is as far as the "final" code as I'm new to github tracking... I'm out of time to keep looking tonight, but my scan of what I think the latest GoTrueClient.ts has dropped the visibility event handler. If I understand correctly this PR was to fix some race condition, but then several issues with token expiring while tab is not in focus or in sleep mode got "pulled in". The visibility handler code was a key part of that possible solution. I think that is gone now, but as I said, this is a maze for me at the moment... My view is that the Supabase client should take care of refreshing the jwt token in all cases where the refresh token is valid without user dev interaction. This can be done right before every authorization header set up in supabase.js, on "token expired" error with a retry on the actual database request, or with visibility handler to deal with dead timer when device MAY have been sleeping (you can not assume "hidden" state means timers have stopped). IMO the refresh token timer should be an optimistic event to save time on an actual database call or realtime status, BUT should NOT be the final solution. Supabase.js and realtime.js should on their own be responsible for verifying token about to expire before they make a request and attempt to refresh it or deal with error gracefully (not easy for realtime). At a minimum the visibility handler is needed to deal with dead timer on mobile (and apparently other situations) when the tab does not have focus or device is asleep and token expires. Once again sorry, it is late but wanted to to throw this out. |
Thanks, Gary, I’m also not happy with this PR. Agreed, I think there’s a couple of things we need to fix
|
|
What kind of change does this PR introduce?
Prevent token refresh race conditions and trigger
recoverAndRefresh
on visibility change.What is the current behavior?
Currently our syncing across multiple tabs causes the tabs to race with each other because the setTimeout is set at exactly the same time when the user logs in in any of the tabs, as it is synced to the other tabs via local storage. All tabs then make the refreshAccessToken call at the same time, causing them to be invalidated.
Related issues: #213 supabase/supabase#6464 supabase/supabase#5990
What is the new behavior?
The new strategy is:
localStorage
lock check to tell other tabs that a refresh request is in progressvisibilitychange
event to clearTimeout and restart autorefreshAdditional context
cc @GaryAustin1 @bnjmnt4n