'Issue with JWT token multiple simultaneously refresh with Refresh token
Tools: .NET 6 with EF Core, Vue 3 with Axios.
R-Token is Refresh Token. DB is database.
I have simple implementation of JWT + Refresh Token auth.
- Client send Login & Password.
- Check password hash in DB.
- If OK, generate JWT token (short lifetime, 1-5 min) and Refresh Token (long lifetime, 365 days) which save to DB.
- Client make requests with JWT.
- When Axios interceptor gets 401, then try to refresh tokens with generated below Refresh Token.
- Used Refresh token deletes from DB, if application cant find R-Token in DB it responses 403.
- 🔃
So, on client, I have some Interval operations which calling server. Sometimes, they executes at the same time, and if JWT token expired I got few request to Refresh tokens in the same time with the same R-tokens.
Issue is: In that situation first request deletes R-token and generate new, then next requests will failed.
What can I do with this problem?
My things about that:
- Do something like singleton in Axios interceptor.
- Somehow use .NET lock construction in backend controller, but for separate clients.
Please help.
Axios interceptor:
instance.interceptors.response.use(response => response,
async (error) => {
const status = error.response ? error.response.status : undefined
const originalRequest = error.config
if(status === 401) {
originalRequest._retry = true
let tryRefresh = await store.dispatch('auth/TryRefreshToken')
if(tryRefresh === false) {
store.dispatch('auth/Logout')
return Promise.reject(error)
}
originalRequest.headers['Authorization'] = 'Bearer ' + store.getters['auth/auth'].accessToken
return instance(originalRequest)
}
if (status === undefined)
{
return Promise.reject(error)
}
return Promise.reject(error)
}
)
.NET Refresh-Token Action in Controller:
[HttpPost, Route("Refresh/{refreshToken}")]
[ProducesResponseType(typeof(AuthenticationResponse), 200)]
public IActionResult RefreshTokens(string refreshToken)
{
Request.Headers.TryGetValue("Authorization", out var accessTokenHeader);
string? accessToken = accessTokenHeader.FirstOrDefault()?.Replace("Bearer", string.Empty).Trim();
if (string.IsNullOrEmpty(accessToken)) return BadRequest("No access token presented.");
JwtSecurityToken? expiredToken = new JwtSecurityTokenHandler().ReadToken(accessToken) as JwtSecurityToken;
if (expiredToken is null) return BadRequest("Bad access token format");
IEnumerable<Claim> claims = expiredToken.Claims;
if (int.TryParse(claims.FirstOrDefault(x => x.Type == "User:Id")?.Value, out int userId) is false)
return BadRequest("No user id in token presented");
User? user = _mainContext.Users.AsNoTrackingWithIdentityResolution()
.Include(x => x.Roles)
.FirstOrDefault(x => x.Id == userId);
if (user is null) return NotFound("No user found");
var userDto = user.ToUserDto();
if (_refreshTokenManager.IsTokenValid(refreshToken, user.Id) is false)
return StatusCode(403);
try {
_refreshTokenManager.RemoveToken(refreshToken);
}
catch (Exception ex) {
Log.Error(ex, "Error in used refresh token deletion.");
}
JwtSettingsDto jwtSettings = _configuration.GetSection("Authorization:Jwt").Get<JwtSettingsDto>();
string newAccessToken = _tokenGeneratorService.GenerateAccessJwtToken(userDto, jwtSettings);
string newRefreshToken = _refreshTokenManager.CreateToken(userDto, Request);
return Ok(new AuthenticationResponse(newAccessToken, newRefreshToken, user.Login, user.DisplayName));
}
Solution 1:[1]
When refresh tokens have a one time use, which is recommended these days, it is a client responsibility to synchronize token refresh. UIs need to do this, if multiple views call APIs concurrently.
It is fairly easy to do this in a utility class that queues promises for token refresh, then only makes the actual HTTP call for the first of them, then returns the same result for all requests.
For an example, see this ConcurrentActionHandler class of mine, used in a React SPA, which is called from this API client code.
In your case it looks like the interceptor class posted by Michael Levy does the same job. so this may be the best option. The same design pattern can be applied to other types of client though, eg mobile apps coded in Swift or Kotlin.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Gary Archer |