JWT Token Refresh Patterns
Implement secure token refresh mechanisms for JWT authentication.
By EMEPublished: February 20, 2025
jwtauthenticationrefresh tokenssecurity
A Simple Analogy
JWT refresh tokens are like temporary passes that can get new passes. Your main pass expires quickly, but you can trade your refresh token for a new one.
Why Token Refresh?
- Short lived: Access tokens expire soon
- Revocation: Invalidate tokens quickly
- Security: Minimize exposure window
- Flexibility: Update permissions
- Logout: Refresh tokens can be blacklisted
Token Structure
public class TokenResponse
{
public string AccessToken { get; set; } // Short-lived (15 min)
public string RefreshToken { get; set; } // Long-lived (7 days)
public int ExpiresIn { get; set; } // 900 seconds
}
Issuing Tokens
public class AuthService
{
private readonly IConfiguration _config;
private readonly IHttpContextAccessor _httpContext;
public AuthService(IConfiguration config, IHttpContextAccessor httpContext)
{
_config = config;
_httpContext = httpContext;
}
public TokenResponse GenerateTokens(User user)
{
var accessToken = GenerateAccessToken(user);
var refreshToken = GenerateRefreshToken();
// Store refresh token in database
user.RefreshToken = refreshToken;
user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7);
return new TokenResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = 900
};
}
private string GenerateAccessToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_config["Jwt:Key"]);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim("role", user.Role)
}),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
}
Refresh Endpoint
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly AuthService _authService;
private readonly IRepository<User> _userRepository;
[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
try
{
// Find user with refresh token
var user = await _userRepository.FirstOrDefaultAsync(
u => u.RefreshToken == request.RefreshToken);
if (user == null)
return Unauthorized("Invalid refresh token");
// Check expiry
if (user.RefreshTokenExpiry < DateTime.UtcNow)
return Unauthorized("Refresh token expired");
// Generate new tokens
var tokens = _authService.GenerateTokens(user);
await _userRepository.UpdateAsync(user);
return Ok(tokens);
}
catch (Exception ex)
{
return Unauthorized(new { message = ex.Message });
}
}
}
public class RefreshTokenRequest
{
public string RefreshToken { get; set; }
}
Client Implementation
// Store tokens
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);
// API interceptor
async function apiCall(url, options = {}) {
let accessToken = localStorage.getItem('accessToken');
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`
}
});
// Token expired?
if (response.status === 401) {
const refreshToken = localStorage.getItem('refreshToken');
const newTokens = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
}).then(r => r.json());
// Update tokens
localStorage.setItem('accessToken', newTokens.accessToken);
localStorage.setItem('refreshToken', newTokens.refreshToken);
// Retry request
return apiCall(url, options);
}
return response.json();
}
Token Rotation
// Rotate tokens on every refresh
public TokenResponse RefreshWithRotation(string oldRefreshToken)
{
var user = ValidateRefreshToken(oldRefreshToken);
// Generate new tokens
var accessToken = GenerateAccessToken(user);
var newRefreshToken = GenerateRefreshToken();
// Invalidate old refresh token
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiry = DateTime.UtcNow.AddDays(7);
return new TokenResponse
{
AccessToken = accessToken,
RefreshToken = newRefreshToken,
ExpiresIn = 900
};
}
Best Practices
- Short access tokens: 15-30 minutes
- Longer refresh tokens: 7-30 days
- Secure storage: HttpOnly cookies
- Rotation: Generate new refresh tokens
- Revocation: Blacklist tokens on logout
Related Concepts
- OAuth 2.0 patterns
- Token blacklisting
- Cookie-based authentication
- Multi-factor authentication
Summary
Token refresh patterns balance security with user experience. Use short-lived access tokens with longer-lived refresh tokens, and rotate tokens on refresh for enhanced security.