Я занимаюсь JWT и их токенами обновления и не могу найти хороший рабочий пример, который одновременно обеспечивает производительность и безопасность.
Производительность :: Он не должен попадать в базу данных при каждом обновлении токена.
Безопасность :: токен обновления должен быть суперсекретным, а не токеном доступа из-за длительного срока службы.
Поэтому я пытаюсь реализовать свой собственный, используя комбинацию заявлений о кешировании в памяти и просроченных токенах:
Шаг 1.
a) После успешного входа в систему создается токен доступа с уникальным GUID в типе утверждения JwtRegisteredClaimNames.Jti.
б) Затем создается токен обновления и сохраняется в memoryCache со связанным значением токена доступа jti (уникальный GUID) в качестве ключа.
c) Оба отправляются в клиентское приложение и сохраняются в localStorage.
Шаг 2.
a) После истечения срока действия токена доступа и токен доступа, и токен обновления отправляются на контроллер обновления.
б) Затем запрос jti в токене с истекшим сроком действия, отправленном в memoryCache в качестве ключа кеширования, для получения токена обновления из памяти.
c) После проверки равенства -send refresh-token и -in-memory refresh-token, если они равны, создается новый экземпляр как токена доступа, так и токена обновления, который отправляется обратно в клиентское приложение.
AuthService.cs
private readonly IConfiguration _configuration;
private readonly IMemoryCache _memoryCache;
private readonly Claim _jtiClaim;
public AuthService(IConfiguration configuration, IMemoryCache memoryCache)
{
_configuration = configuration;
_memoryCache = memoryCache;
_jtiClaim = new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString());
}
public string GenerateAccessToken(IList<Claim> claims)
{
claims.Add(_jtiClaim);
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtConfiguration:JwtKey"]));
var jwtToken = new JwtSecurityToken(
issuer: _configuration["JwtConfiguration:JwtIssuer"],
audience: _configuration["JwtConfiguration:JwtIssuer"],
claims: claims,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(int.Parse(_configuration["JwtConfiguration:JwtExpireMins"])),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(jwtToken);
}
public string GenerateRefreshToken(ClientType clientType)
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
var token = Convert.ToBase64String(randomNumber);
var refreshToken = JsonConvert.SerializeObject(new RefreshToken(token, _jtiClaim.Value, clientType));
_memoryCache.Set(_jtiClaim.Value, refreshToken, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromDays(7)));
return token;
}
}
public RefreshToken GetRefreshToken(string jtiKey)
{
if (!_memoryCache.TryGetValue(jtiKey, out string refreshToken)) return null;
_memoryCache.Remove(jtiKey);
return JsonConvert.DeserializeObject<RefreshToken>(refreshToken);
}
public ClaimsPrincipal GetPrincipalFromExpiredToken(string accessToken)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtConfiguration:JwtKey"])),
ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(accessToken, tokenValidationParameters, out var securityToken);
if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
AuthController.cs
private readonly SignInManager<User> _signInManager;
private readonly UserManager<User> _userManager;
private readonly AuthService _authService;
private readonly IMemoryCache _memoryCache;
private readonly DataContext _context;
public AuthController(UserManager<User> userManager, AuthService authService,
SignInManager<User> signInManager, DataContext context)
{
_userManager = userManager;
_authService = authService;
_signInManager = signInManager;
_context = context;
}
[HttpPost]
public async Task<ActionResult> Login([FromBody] LoginDto model)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);
if (!result.Succeeded) return BadRequest(new { isSucceeded = result.Succeeded, errors= "INVALID_LOGIN_ATTEMPT" });
var appUser = _userManager.Users.Single(r => r.Email == model.Email);
return Ok(new
{
isSucceeded = result.Succeeded,
accessToken = _authService.GenerateAccessToken(GetClaims(appUser)),
refreshToken = _authService.GenerateRefreshToken(model.ClientType)
});
}
[HttpPost]
public ActionResult RefreshToken([FromBody] RefreshTokenDto model)
{
var principal = _authService.GetPrincipalFromExpiredToken(model.AccessToken);
var jtiKey = principal.Claims.Single(a => a.Type == JwtRegisteredClaimNames.Jti).Value;
var refreshToken = _authService.GetRefreshToken(jtiKey);
if (refreshToken == null)
return BadRequest("Expired Refresh Token");
if (refreshToken.Token != model.RefreshToken)
return BadRequest("Invalid Refresh Token");
return Ok(new
{
isSucceeded = true,
accessToken = _authService.GenerateAccessToken(principal.Claims.SkipLast(1).ToList()),
refreshToken = _authService.GenerateRefreshToken(model.ClientType)
});
}
Я не уверен, что это хорошая реализация для токена обновления, потому что токен обновления может быть скомпрометирован в клиентском приложении.
Вы можете предложить мне лучшее решение?