JWT authentication is widely used to secure ASP.NET Core REST APIs because it is stateless, scalable, and suitable for distributed systems. However, short-lived access tokens alone are not sufficient for production-grade authentication systems. To maintain security while providing a seamless user experience, refresh tokens are implemented alongside access tokens.
This article provides a comprehensive, production-ready guide to implementing refresh tokens with JWT in ASP.NET Core, covering database design, token generation and validation, rotation strategy, revocation handling, security best practices, and real-world architectural considerations.
Why Refresh Tokens Are Needed
Access tokens are intentionally short-lived (for example, 15–30 minutes) to reduce security risk if compromised. However, forcing users to log in repeatedly would create a poor user experience.
Refresh tokens solve this problem by:
Allowing issuance of new access tokens without re-authentication
Improving security with short-lived access tokens
Supporting secure session continuation
Enabling token revocation control
Access Token vs Refresh Token
| Parameter | Access Token | Refresh Token |
|---|
| Purpose | Access protected APIs | Obtain new access token |
| Lifetime | Short (minutes) | Long (days/weeks) |
| Sent With API Calls | Yes | No |
| Storage | Memory / secure storage | HttpOnly secure cookie or secure store |
| Revocation Strategy | Expiration-based | Database-based control |
| Security Sensitivity | High | Very High |
Access tokens authorize API access, while refresh tokens extend authentication sessions securely.
Recommended Architecture
Authentication Flow:
User logs in with credentials.
Server validates credentials.
Server generates:
Short-lived access token
Long-lived refresh token
Refresh token is stored securely in database.
Client stores access token in memory.
When access token expires, client sends refresh token to obtain new access token.
Step 1: Create Refresh Token Model
Create a database entity to store refresh tokens.
public class RefreshToken
{
public int Id { get; set; }
public string Token { get; set; }
public string UserId { get; set; }
public DateTime ExpiryDate { get; set; }
public bool IsRevoked { get; set; }
public DateTime CreatedAt { get; set; }
}
Each refresh token should be uniquely stored and associated with a user.
Step 2: Generate Access and Refresh Tokens
Access Token Generation:
private string GenerateAccessToken(IEnumerable<Claim> claims)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Issuer"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(15),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Refresh Token Generation:
private string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
Use cryptographically secure random generators.
Step 3: Save Refresh Token in Database
var refreshToken = new RefreshToken
{
Token = GenerateRefreshToken(),
UserId = user.Id,
ExpiryDate = DateTime.UtcNow.AddDays(7),
CreatedAt = DateTime.UtcNow,
IsRevoked = false
};
_context.RefreshTokens.Add(refreshToken);
await _context.SaveChangesAsync();
Never store refresh tokens in plain text in production systems without hashing.
Step 4: Return Tokens to Client
return Ok(new
{
accessToken = accessToken,
refreshToken = refreshToken.Token
});
Best practice: store refresh token in HttpOnly secure cookie.
Step 5: Create Refresh Endpoint
[HttpPost("refresh")]
public async Task<IActionResult> Refresh(TokenRequest request)
{
var storedToken = await _context.RefreshTokens
.FirstOrDefaultAsync(x => x.Token == request.RefreshToken);
if (storedToken == null || storedToken.IsRevoked || storedToken.ExpiryDate < DateTime.UtcNow)
return Unauthorized();
var user = await _userManager.FindByIdAsync(storedToken.UserId);
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, "User")
};
var newAccessToken = GenerateAccessToken(claims);
return Ok(new { accessToken = newAccessToken });
}
Step 6: Implement Refresh Token Rotation
Token rotation improves security by issuing a new refresh token each time.
Process:
Mark old refresh token as revoked.
Generate new refresh token.
Save new token in database.
Example:
storedToken.IsRevoked = true;
var newRefreshToken = new RefreshToken
{
Token = GenerateRefreshToken(),
UserId = storedToken.UserId,
ExpiryDate = DateTime.UtcNow.AddDays(7),
CreatedAt = DateTime.UtcNow
};
_context.RefreshTokens.Add(newRefreshToken);
await _context.SaveChangesAsync();
This prevents replay attacks.
Step 7: Token Revocation on Logout
[HttpPost("logout")]
public async Task<IActionResult> Logout(string refreshToken)
{
var token = await _context.RefreshTokens
.FirstOrDefaultAsync(x => x.Token == refreshToken);
if (token != null)
{
token.IsRevoked = true;
await _context.SaveChangesAsync();
}
return Ok();
}
This ensures compromised tokens cannot be reused.
Security Best Practices
Use HTTPS only
Store refresh tokens in HttpOnly cookies
Hash refresh tokens before saving
Use short-lived access tokens
Implement token rotation
Detect reuse of revoked refresh tokens
Apply rate limiting on refresh endpoint
Monitor suspicious token usage
Common Mistakes
Using long-lived access tokens
Not storing refresh tokens in database
Not revoking tokens on logout
Storing refresh tokens in local storage
Not implementing rotation strategy
Real-World Production Scenario
In a cloud-based SaaS application:
Access token lifetime: 15 minutes
Refresh token lifetime: 7 days
Tokens stored securely
Rotation enabled
Revocation on password change
Suspicious refresh token reuse triggers account lock
This approach balances usability and security.
Summary
Implementing refresh tokens with JWT in ASP.NET Core involves issuing short-lived access tokens alongside long-lived refresh tokens stored securely in a database, validating refresh tokens through a dedicated endpoint, rotating tokens upon use, and revoking them when necessary. By combining cryptographically secure token generation, database-backed revocation control, HttpOnly cookie storage, token rotation strategies, and strict expiration policies, developers can build secure, scalable, and production-ready authentication systems that protect REST APIs while maintaining a seamless user experience.