2

I'm developing a web application that uses tokens for authentication. Users often open multiple browser tabs, and each tab has a client-side timer to refresh the authentication tokens before they expire. The tokens are stored in cookies and are set via the backend (Golang). We use refresh tokens to extend the current session by issuing a new refresh token and revoking the previous one.

The problem:

When two or more tabs simultaneously send a refresh request using the same refresh_token, the first request succeeds, and the server issues new tokens while revoking the old refresh_token. Subsequent requests from other tabs using the now-revoked refresh_token fail with a 401 Unauthorized error, causing those tabs to log the user out.

Managing synchronization across tabs adds complexity, and I'm not sure if this needs to be handled server-side or client-side.

We aim to keep the user logged in while they are active. When the user closes the website, we want to keep the session alive for about 5 more minutes so that if the user returns, they don't have to log in again. The main logic of our website uses a WebSocket (only in certain tabs) to transfer data between the client and server, and no API calls are being made during this time, so the user session can expire. To address this, we decided to call the refresh API every 30 seconds to keep the user logged in, which exacerbated the issue of simultaneous refresh API calls and the associated bugs. Additionally, we need to monitor the user session and terminate it when appropriate, so we cannot use stateless tokens.

Are there alternative strategies to handle token refresh in a multi-tab environment that avoid these synchronization issues?

Would it be feasible to remove refresh tokens altogether and rely on short-lived access tokens with traditional server-side sessions? What are the trade-offs in terms of security, scalability, and user experience?

Backend: Golang Frontend: React I'm looking for guidance on how to redesign our authentication flow or adopt a different approach that gracefully handles multiple tabs without conflicting refresh requests, while ensuring security and a seamless user experience.

6
  • If a request on the other tab fails, check the cookie. If it was changed (by the first tab) dont log out. Would this work for you?
    – pschill
    Commented Nov 10 at 10:49
  • @pschill having shared cookie or storage was our first idea, like putting flags and stopping apis from calling backend, and when response is ok or 401 then share that response between all tabs, but this really adds a lot of complexity, so we started to think mb we are over engineering this and we don't need refresh tokens at all. so I'm trying to find best approach here.
    – Bad Boy
    Commented Nov 10 at 10:52
  • seems like if you store the refresh and auth token in a cookie you already have a shared token, its the auto refresh which is breaking things
    – Ewan
    Commented Nov 10 at 12:05
  • You might want to consider why you're using refresh tokens in the first place. What's your threat model? Refresh tokens make sense if you don't just have a single backend, but separate auth servers and resource servers. In this multi-party scenario, access tokens cannot be revoked, so the next best thing is to limit their lifespan. If access gets revoked, the client will get a permission error at next refresh. All of this is unnecessary when you have a single server and can thus use session tokens that you can immediately revoke server-side when necessary (e.g. 5 min after last interaction).
    – amon
    Commented Nov 10 at 14:24
  • @amon but lets say you have one thousand sessions, how do you revoke all of them when they go idle? and how to keep them alive if they are active ?
    – Bad Boy
    Commented Nov 10 at 15:08

3 Answers 3

2

OK I'm going to rewrite this with more detail.

SO from your post it seems like you are just using the "refresh API every 30 seconds" call to naturally repopulate the cookie tokens with no logic and using the cookies sent by normal requests to communicate with the server.

Normally with a react or other SPA application I would expect communication to be done via api calls initiated from javascript functions. This allows you more control over the process.

So when you click a button react calls some method

function onButtonClick()
{
  var data = fetch('myapi',{
    headers: {Authorization: 'Bearer {getToken()}'}
    })
  
}

function getToken()
{
   if(getExpiryFromCookie() - bufferSeconds > now()) { refreshToken();)
   
   token = GetTokenFromCookie();       
   return token;
}

function refreshToken()
{
    var refreshToken = GetRefreshFromCookie();
    try {
      var data  = api.RefreshToken(refreshToken);
      SetRefreshCookie(data.refresh);
      SetTokenCookie(data.access);
      SetExpiryCookie(data.expiry);
    }
    catch
    {
       //someone else has refreshed!
       //check refresh token expiry, maybe its gone stale somehow
       if(refresh is stale)
       {
          //logout
       }
       else
       {
          //nothing, use access token from cookie.
       }
    }
}

Now on page load, you can set a timer to call refreshToken at expiry - bufferSeconds with a randomly chosen buffer seconds. so you don't have the extra every 30sec traffic, AND the likelihood of conflicting with another tab is reduced.

Access Tokens should always be valid. if you pick one up mid refresh the old token will still be valid

Refresh Tokens can be invalid due to the race condition. In which case you just ignore the error, or because the app has crashed or just not run in ages, in which case you can redirect to login

20
  • that does work on couple of tabs, but still users can open a lot of tabs on different sections of website, Yes the probability of overlapping is low, but having a lot of tabs keep refreshing the token does not seem right. right? like if a user opens 30 tabs, there will be 30 different refresh tokens and might cause some other problems that i can't think of now :D. just doesn't seem right to allow a lot of refresh requests.
    – Bad Boy
    Commented Nov 10 at 12:37
  • @BadBoy, you will never eliminate race conditions. You can only make them unlikely. This is a common problem any time there is a shared resource and multiple clients or people who need access to it. Commented Nov 10 at 15:24
  • if you have 30 tabs open and check say every 10 sec, theres a good chance you will have 3 tabs checking in the same second, they then have to roll lower than the remaining time to expire. even then one fails and picks up the new token from the cookie afterwards. you dont have 30 tokens because the tabs all share the same cookie store
    – Ewan
    Commented Nov 10 at 15:28
  • 1
    @Ewan what? we are doing api calls, like client side (react with fetch) calls backend apis. it calls the /api/refresh_token, then backend refreshes the refresh token and extends the user session
    – Bad Boy
    Commented Nov 10 at 15:49
  • 1
    @Ewan Yes I re-read the problem and came to that conclusion, which is why I added an answer where this is controlled via a flag to inform all the sessions if they need to wait before dispatching an API request, in the case that a refresh token request is in progress, which is not ideal, but is 100% reliable. Having a random wait interval as you suggest is a fair option but given enough sessions, for example over 30 sessions, will eventually lead to a sort of "race" between two sessions resulting in a conflict.
    – Dimitar
    Commented Nov 20 at 13:27
1

So you have several contexts that use the same token and refresh should be called only once and refreshes everything. You refresh the token based on time.

The normal method would be: You start a token refresh, take notice that this token must not be refreshed again, get notified when the token is either refreshed or the refresh is rejected. At that point either the token is unusable (refresh rejected), or you can technically do another refresh (but of course you want to wait say 30 seconds).

Now you have multiple contexts with the same token, and want to change your code minimally.

For every unique token, you note that a refresh is running, or the token is unusable. Now any context wants a refresh at a random time. You find your unique token, and protected by a lock you make your decision what to do. If the token is unusable, that's it. If if it currently being refreshed, there is nothing you want to do but you append yourself to a list of contexts that want to be informed. If it is not currently being refreshed, you mark it under the lock as "being refreshed" then outside the lock you start the refresh. Important that your time under the lock is not even a microsecond and you do nothing time consuming under the lock, that's done outside.

When a refresh finishes, it checks under the lock what to do. Notifies everyone that the token is refreshed or now unusable.

The important thing is to have one data structure per unique token. And this data structure does what usually a refresh would do. And I would think it is better if the server tells you say a second ahead that the token needs a refresh.

2
  • Thanks for this complete answer, and yes this appproach is the one that i actually implemented yesterday, I could not find anything better. So i have one slice for storing tokens with Locks, and then another map for storing the result for that token with Locks. The only thing that i was not sure about was that if i should use redis and its locking mechanism to handle this for me or should i implement the locking system all by myself...???
    – Bad Boy
    Commented Nov 20 at 13:20
  • I also had to add a cleanup job to remove that refresh token from slices, you can not store them forever. it will just keep expanding.
    – Bad Boy
    Commented Nov 20 at 13:22
0

Assuming that all the tabs run on the same browser and therefore can share cookie storage, the following mechanism can be implemented:

  1. Upon session start, every tab obtains it's own unique_tab_id.
  2. The first tab is assigned as a tab_in_charge, who will dispatch everything there is to do regarding authentication.
  3. When another tab needs to refresh the token, the tab sends a request to the tab_in_charge via a cookie or local storage.
  4. The tab_in_charge has the obligation to check every X seconds if a refresh request has been submitted by another tab.
  5. If there is more than one request to refresh the token, then it only accepts the first and rejects the other, as when the new access token is received, then all of the tabs will receive that token.

Another thing I would add here is Proactive Token Refreshing. You can use the iat and exp data from the token payload to calculate when it will expire, therefore giving you info about how much time you have. You can make it request a new access token 45 seconds before it expires, and adding this information in the request you send to the tab_in_charge. This will ensure resilient communication with your sockets, making sure there is zero down time while waiting for a new token or restarting the connection.

Finally, you need to gracefully transfer the position of tab_in_charge to another tab if you are closing the current.

Not the answer you're looking for? Browse other questions tagged or ask your own question.