ASP.NET Core  

How to Implement Refresh Tokens with JWT in ASP.NET Core?

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

ParameterAccess TokenRefresh Token
PurposeAccess protected APIsObtain new access token
LifetimeShort (minutes)Long (days/weeks)
Sent With API CallsYesNo
StorageMemory / secure storageHttpOnly secure cookie or secure store
Revocation StrategyExpiration-basedDatabase-based control
Security SensitivityHighVery High

Access tokens authorize API access, while refresh tokens extend authentication sessions securely.

Recommended Architecture

Authentication Flow:

  1. User logs in with credentials.

  2. Server validates credentials.

  3. Server generates:

    • Short-lived access token

    • Long-lived refresh token

  4. Refresh token is stored securely in database.

  5. Client stores access token in memory.

  6. 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:

  1. Mark old refresh token as revoked.

  2. Generate new refresh token.

  3. 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.