I got a reactjs site and asp 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?
I got a reactjs site and asp 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?
Share Improve this question asked May 10, 2019 at 17:49 chobo2chobo2 85.9k207 gold badges551 silver badges861 bronze badges 8- When you get an access and refresh tokens for two tabs, are those the same ones or do you get two distinct copies? Also, can't you also store the last time the two were set in local storage and have the timer functions check against that before they execute? – VLAZ Commented May 10, 2019 at 17:57
- 1st tab opens, get the access/refresh, 2nd tab opened and uses the access/refresh gotten by 1st tab as localstorage is shared. So only got 1. If I would have 2 that would mean every time would have to relogin or something – chobo2 Commented May 10, 2019 at 17:59
- Can't you set the expiration time of the tokens on CreateRefreshToken and use those Unix Timestamps to check for the token validity? – Carlos Alves Jorge Commented May 10, 2019 at 18:03
- but then if the person es say after the expiary then they are auto logged out, which is not the desired oute as they should be able to e back a week later with refresh token and still be able to login. I need to be able to handle autologin but also handle multiple tabs being opened at the same time. – chobo2 Commented May 10, 2019 at 18:06
- You need to set a time for when the user needs to relogin... If it's a week your refresh token needs to be a week long so that you can create new tokens in the meanwhile... Usually refresh tokens have a very long longevity while tokens have short – Carlos Alves Jorge Commented May 10, 2019 at 18:11
3 Answers
Reset to default 4Best 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 mon 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 plete 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.
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 paring 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 promised, the hacker only has say 10 minutes before he would need the refresh_token to generate a new one...
I use the page Visibility API https://developer.mozilla/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);
}