chobo2
chobo2

Reputation: 85715

Stop Race Condition of Refresh Token?

I got a reactjs site and asp.net core backend and I am having a problem with refresh tokens.

When someone logs into my site they are given a access token and refresh token (pretty standard). Now I set a timer that is shorter than the time of the access token.

This all works great expect when they have multiple tabs open. The problem is they all share the localstorage(need to have autologin so can't use session storage)

Scenario

2 tabs open one after another. 2 timers are set 2 mins before the access token dies.

1st time fires first sending the refresh token to the server and brings back the a new refresh/access token. On the sever the sent refresh token is removed.

2nd timer fires shortly after the first(while the first is working) but it is now very possible that refresh token has been removed, making this request invalid.

How do I stop this race condition?

var foundRefreshToken = dbContext.Tokens.FirstOrDefault(x => x.Value == refreshToken);

if (foundRefreshToken == null)
{
    return null;
}

var newRefreshToken = CreateRefreshToken(foundRefreshToken.ClientId, foundEmployee.Id);

dbContext.Tokens.Remove(foundRefreshToken);
dbContext.Tokens.Add(newRefreshToken);
dbContext.SaveChanges();


private Token CreateRefreshToken(string clientId, string userId)
    {
        return new Token()
        {
            ClientId = clientId,
            EmployeeId = userId,
            Value = GenerateRefreshToken(),
            CreatedDate = DateTime.UtcNow
        };
    }

// high level js
  refreshTimer;
  setRefreshTimer(intervals) {
    this.clearRefreshTimer();
    this.refreshTimer = setInterval(() => {
      this.refreshAuthentication();
    }, intervals);
  }

The only 2 things, I can think of is don't remove the refresh token(but this will cause problems with the auto login)

Or I have a flag in the local storage that "locks" the 1st tab to do the refresh and the others wait to see if it does it or not(guess need another timer). If not then the next one tries.

Anyone else got any other ideas?

Upvotes: 7

Views: 5967

Answers (3)

Andrey K.
Andrey K.

Reputation: 673

I use the page Visibility API https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API for this purpose. When tab is inactive I cancel renewal subscription and setup for renewal when tab is active again.

My code for Angular is:

  constructor(
    private router: Router,
    private http: HttpClient,
    private tokenService: TokenService,
    @Inject(DOCUMENT) private readonly documentRef: Document,
    ) {
    this.getToken(); // From local Browser store
    this.loggedIn = this.isAuthenticated();
    this.status = new BehaviorSubject<boolean>(this.loggedIn);
    this.documentRef.addEventListener("visibilitychange", () => {
      console.log(document.hidden, document.visibilityState);
      if (document.hidden) {
        this.cancelRenewal();
      } else {
        this.getToken();
        this.loggedIn = this.isAuthenticated();
        this.status.next(this.loggedIn);
        this.scheduleRenewal();
      }
    }, false);
  }

Upvotes: 0

Steve McCollom
Steve McCollom

Reputation: 171

Best practice requires that a refresh token should only be usable once and a new one issued whenever it is used. An attempt to use the old one again should be considered a stolen token - all outstanding tokens for that user should be invalidated and any new access attempts should require a full login.

A race condition occurs when two sessions share a common refresh token (like when two tabs are open in a browser and the token is stored in a http-only cookie). When a condition occurs where both sessions attempt a refresh at the same time using the same refresh token, the first one to the server gets a valid new token, but the second one finds their token is now invalid and gets logged out.

As the OP mentioned, this can be solved on the front end by using a mechanism like a busy flag so that the first refresh must complete before the second can proceed.

On the back end, you can have a mechanism that allows the refresh token to be reused for a very short period of time (only a few seconds) before it is fully invalidated or deleted.

Upvotes: 3

Carlos Alves Jorge
Carlos Alves Jorge

Reputation: 1985

When you create the token and the refresh tokens both should have an expiration date like:

return new Token()
        {
            ClientId = clientId,
            EmployeeId = userId,
            Value = GenerateRefreshToken(),
            CreatedDate = DateTime.UtcNow,
            ExpirationDate = <you decide>
        };

On every request you should check if your token is expired by comparing dates. If it is expired you can use keep the user authenticated. Ultimately you could even never expire the refresh token since it must be stored securely by your application.

The idea behind the refresh token and short lived tokens is, in case a token is compromised, the hacker only has say 10 minutes before he would need the refresh_token to generate a new one...

Upvotes: 1

Related Questions