Authentication doesn’t end after issuing a JWT. Real-world applications must handle token expiration, session continuity, and security threats like token theft.
In this article, we’ll explore Refresh Tokens and how to build secure session management in ASP.NET Core.
Why Refresh Tokens Are Important
JWT access tokens are:
- Stateless
- Short-lived
- Not revocable easily
If access tokens never expire → huge security risk
If they expire too quickly → bad user experience
👉 Refresh tokens solve this problem
Token Strategy (Best Practice)
| Token Type | Lifetime | Purpose |
| ------------- | ------------ | -------------------- |
| Access Token | 5–15 minutes | API authorization |
| Refresh Token | Days / Weeks | Get new access token |
How Refresh Tokens Work (Flow)
- User logs in
- Server issues:
- Access Token (JWT)
- Refresh Token (secure random string)
- Client stores:
- Access Token → memory
- Refresh Token → HTTP-only cookie
- Access token expires
- Client calls
/refresh - Server validates refresh token
- New access token is issued
Designing a Secure Refresh Token System
🔐 Key Security Principles
- Refresh tokens must be:
- Random
- Stored server-side
- Revocable
- Use token rotation
- Store refresh tokens hashed in DB
- Invalidate tokens on:
- Logout
- Password change
- Suspicious activity
Database Model Example
public class RefreshToken
{
public int Id { get; set; }
public string TokenHash { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsRevoked { get; set; }
public string UserId { get; set; }
}Generating a Secure Refresh Token
public string GenerateRefreshToken()
{
var randomBytes = RandomNumberGenerator.GetBytes(64);
return Convert.ToBase64String(randomBytes);
}Hash Before Storing
public string HashToken(string token)
{
using var sha256 = SHA256.Create();
return Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(token)));
}Login Endpoint Example
[HttpPost("login")]
public async Task<IActionResult> Login(LoginDto dto)
{
// Validate user credentials
var accessToken = GenerateJwt(user);
var refreshToken = GenerateRefreshToken();
SaveRefreshToken(user.Id, HashToken(refreshToken));
Response.Cookies.Append("refreshToken", refreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddDays(7)
});
return Ok(new { accessToken });
}Refresh Token Endpoint
[HttpPost("refresh")]
public IActionResult Refresh()
{
var refreshToken = Request.Cookies["refreshToken"];
if (string.IsNullOrEmpty(refreshToken))
return Unauthorized();
var tokenHash = HashToken(refreshToken);
var storedToken = _db.RefreshTokens.FirstOrDefault(t => t.TokenHash == tokenHash);
if (storedToken == null || storedToken.IsRevoked || storedToken.ExpiresAt < DateTime.UtcNow)
return Unauthorized();
// Rotate token
storedToken.IsRevoked = true;
var newRefreshToken = GenerateRefreshToken();
SaveRefreshToken(storedToken.UserId, HashToken(newRefreshToken));
var newAccessToken = GenerateJwt(user);
Response.Cookies.Append("refreshToken", newRefreshToken, cookieOptions);
return Ok(new { accessToken = newAccessToken });
}Token Rotation (Highly Recommended)
Every refresh:
- Old refresh token → revoked
- New refresh token → issued
Why?
- Prevents replay attacks
- Stops stolen token reuse
Secure Session Management Tips
✅ Best Practices
- Use HTTPS only
- Store refresh token in HttpOnly cookie
- Short access token lifespan
- Rotate refresh tokens
- Log refresh attempts
- Revoke all tokens on logout
Logout Implementation
[HttpPost("logout")]
public IActionResult Logout()
{
var refreshToken = Request.Cookies["refreshToken"];
RevokeRefreshToken(refreshToken);
Response.Cookies.Delete("refreshToken");
return Ok();
}Common Mistakes to Avoid
❌ Storing refresh tokens in localStorage
❌ Long-lived access tokens
❌ No token revocation
❌ Reusing refresh tokens
❌ Not hashing tokens in DB
Real-World Use Cases
- Banking apps
- SaaS dashboards
- Mobile apps
- Admin panels
- Social platforms