Isaac.

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

  1. Short access tokens: 15-30 minutes
  2. Longer refresh tokens: 7-30 days
  3. Secure storage: HttpOnly cookies
  4. Rotation: Generate new refresh tokens
  5. 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.