JWT Token Refresh Patterns
Implement secure token refresh mechanisms for JWT authentication.
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.
Related Articles
JSON Web Tokens (JWT) Advanced
Master JWT claims, validation, and practical implementation patterns.
Read More securitySecuring Your Web API
Learn how to protect your web APIs from threats using authentication, authorization, encryption, and best practices.
Read More securityAPI Authentication: OAuth 2.0
Understand OAuth 2.0: secure delegation of access without sharing passwords.
Read More