Implementing JWT Authentication with Refresh Tokens in ASP.NET Core Web API

Securing your APIs is more important than ever. In this guide, you’ll learn how to implement JWT Authentication and Refresh Tokens in a ASP.NET Core Web API, a must-know skill for any backend developer working with .NET technologies.
Whether you're building with ASP.NET Core MVC, Web API, or developing enterprise-level systems, understanding JWT Authentication is essential to building secure, scalable applications.
Let’s walk through how to code secure endpoints using ASP.NET Core JWT token authentication, complete with refresh token support.
If you would rather follow along with the complete implementation, the full JWT auth and refresh-token setup - signing, rotation, and revocation - is on Patreon, ready to run. Chapter 7 of the ASP.NET Core Web API: Zero to Production course builds exactly this inside a real API.
https://www.youtube.com/watch?v=vTXXdm44IdQ
🔐 Why Use JWT with Refresh Tokens?
In ASP.NET Core JWT Authentication, tokens allow stateless authentication. That means your API doesn’t need to store session data, which improves performance and scalability.
However, JWT tokens have expiration times. That’s why we implement Refresh Token JWT functionality. So users don’t need to log in repeatedly when their access token expires.
Using ASP.NET Core JWT Refresh Token implementation ensures a smooth and secure user experience.
💡 A quick note before we dive in: JWT with refresh tokens is one piece of a larger puzzle. Getting the implementation right in isolation is one thing - knowing how it fits alongside Clean Architecture, global error handling, rate limiting, and health checks in a real production API is another. If you want to see the complete picture, Chapter 7 of the ASP.NET Core Web API: Zero to Production course covers this exact pattern end-to-end, including token rotation, revocation strategies, and secure storage - all as part of a working codebase you can download and run.
🛠 Setting Up ASP.NET Core for JWT
To start using ASP.NET Core JWT, install these essential libraries:
System.IdentityModel.Tokens.JwtMicrosoft.AspNetCore.Authentication.JwtBearer
This demo uses SQL Server and a well-structured repository pattern for handling user and token data.
Whether you're working with ASP.NET Core MVC JWT token authentication or a lightweight ASP.NET Core API JWT service, these steps apply universally.
🧱 Project Structure Overview
Entity Classes:
User,RefreshTokenRepositories: Encapsulate CRUD operations
DbContext: Includes
DbSet<User>andDbSet<RefreshToken>Admin Seeding: An initial user is seeded for testing
🔧 Configure JWT Token Authentication
In appsettings.json, define your token configuration:
"JwtConfig": {
"Issuer": "http://localhost:5280/",
"Audience": "http://localhost:5280/",
"Key": "UhdMzMEY0l1pv5oRMLQhSLJa2Bh0qVDFjmKxe6mGsbEJoQkHiW5Qjd8DzFT7IX7kQ4WHfcLB0GhER448I0FzNsAaygJEsKfny4rw",
"TokenValidityMins": 10,
"RefreshTokenValidityMins": 30
}
In Program.cs, configure ASP.NET Core JWT token authentication like this:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["JwtConfig:Issuer"],
ValidAudience = builder.Configuration["JwtConfig:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtConfig:Key"]!)),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
});
builder.Services.AddAuthorization();
This setup works whether you’re building a Dotnet Core JWT Authentication service or a large-scale dotnet system.
Don't forget to include Authentication and Authorization middleware in the pipeline.
app.UseAuthentication();
app.UseAuthorization();
👤 Coding the Login Flow
You'll need three models:
LoginRequest(Username, Password)LoginResponse(JWT Token, Refresh Token, Expiry)RefreshResponse(Token)
public class LoginRequestModel
{
public string? Username { get; set; }
public string? Password { get; set; }
}
public class LoginResponseModel
{
public string? UserName { get; set; }
public string? AccessToken { get; set; }
public int ExpiresIn { get; set; }
public string? RefreshToken { get; set; }
}
public class RefreshRequestModel
{
public string? Token { get; set; }
}
The JWT token is generated using JwtSecurityTokenHandler. You’ll include claims like username and sign it using the configured secret key.
With DotNet core JWT, make sure passwords are hashed properly. A custom PasswordHandler helps keep your users’ credentials secure.
🔄 Implementing Refresh Token Logic
Here’s how to build a Refresh Token system in your .NET Web API:
Generate a GUID-based refresh token
Save it in the database with an expiry time
During login, generate and return both JWT and refresh token
Create a method to validate the refresh token
If valid, issue a new JWT and a new refresh token
This process is standard in modern ASP.Net Core API JWT Authentication implementations.
public class JwtAuthenticationService
{
private readonly IConfiguration _configuration;
private readonly UserRepository _userRepository;
private readonly RefreshTokenRepository _refreshTokenRepository;
public JwtAuthenticationService(IConfiguration configuration,
UserRepository userRepository,
RefreshTokenRepository refreshTokenRepository)
{
_configuration = configuration;
_userRepository = userRepository;
_refreshTokenRepository = refreshTokenRepository;
}
public async Task<LoginResponseModel?> Authenticate(LoginRequestModel request)
{
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
return null;
var user = await _userRepository.Get(request.Username);
if (user is null || !PasswordHashHandler.VerifyPassword(request.Password, user.PasswordHash!))
return null;
return await GenerateJwtToken(user);
}
public async Task<LoginResponseModel?> ValidateRefreshToken(string token)
{
var refreshToken = await _refreshTokenRepository.Get(token);
if (refreshToken is null || refreshToken.Expiry < DateTime.UtcNow)
return null;
await _refreshTokenRepository.Delete(refreshToken);
var user = await _userRepository.Get(refreshToken.UserId);
if(user is null) return null;
return await GenerateJwtToken(user);
}
private async Task<LoginResponseModel> GenerateJwtToken(User user)
{
var issuer = _configuration["JwtConfig:Issuer"];
var audience = _configuration["JwtConfig:Audience"];
var key = Encoding.ASCII.GetBytes(_configuration["JwtConfig:Key"]!);
var tokenValidityMins = _configuration.GetValue<int>("JwtConfig:TokenValidityMins");
var tokenExpiryTimeStamp = DateTime.UtcNow.AddMinutes(tokenValidityMins);
var token = new JwtSecurityToken(issuer,
audience,
[
new Claim(JwtRegisteredClaimNames.Name, user.Username!)
],
expires: tokenExpiryTimeStamp,
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha512Signature));
var accessToken = new JwtSecurityTokenHandler().WriteToken(token);
return new LoginResponseModel
{
UserName = user.Username,
AccessToken = accessToken,
ExpiresIn = (int)tokenExpiryTimeStamp.Subtract(DateTime.UtcNow).TotalSeconds,
RefreshToken = await GenerateRefreshToken(user.Id)
};
}
private async Task<string> GenerateRefreshToken(int userId)
{
var refreshTokenValidityMins = _configuration.GetValue<int>("JwtConfig:RefreshTokenValidityMins");
var refreshToken = new RefreshToken
{
Token = Guid.NewGuid().ToString(),
Expiry = DateTime.UtcNow.AddMinutes(refreshTokenValidityMins),
UserId = userId
};
await _refreshTokenRepository.Create(refreshToken);
return refreshToken.Token;
}
}
⚙ Background Cleanup for Expired Tokens
To avoid database bloat, expired refresh tokens should be cleaned up periodically.
For this demo, a Background Service is implemented that:
Runs every hour
Deletes all expired tokens
✨ You can use alternatives like Hangfire, Quartz.NET, or SQL Agent for more advanced scheduling.
The complete source code and implementation guide are available to Coding Droplets members on Patreon.
🧪 Testing with Postman & Swagger
Use Postman to authenticate and retrieve tokens.
Test the refresh endpoint by passing the refresh token.
Swagger is also configured to support JWT authentication.
Once authorized with a token in Swagger, you can test all secured endpoints directly.
🧠 Summary
Here’s what you’ve learned:
How to Code secure APIs using ASP.NET Core JWT Token
Why and how to implement a Refresh Token JWT mechanism
How to manage token expiration using Dotnet Core JWT Refresh Token
Best practices for .NET API Authentication
How to integrate it all in your Web API with Swagger
Whether you’re just starting out or leveling up your coding skills, understanding JWT Authentication is a game-changer.
FAQ
How long should a JWT access token live? Keep access tokens short-lived - around 15 minutes is common. A short lifetime limits the damage if a token leaks, and the refresh token handles seamless renewal so users are not forced to log in repeatedly.
What is refresh token rotation and why does it matter? Rotation issues a brand-new refresh token every time one is used and invalidates the old one. If an attacker replays a stolen refresh token, the reuse is detected and the whole token family can be revoked - which is far safer than a long-lived, reusable refresh token.
Where should I store refresh tokens? Store them server-side (for example in your database) so they can be revoked, and deliver them to clients in a secure, HttpOnly cookie where possible. Never put long-lived tokens in localStorage, where any XSS can read them.
How do I revoke a JWT before it expires? Access tokens are stateless, so you cannot revoke an individual one directly - keep them short-lived instead. For immediate revocation, invalidate the associated refresh token in your store and, for high-security needs, maintain a small deny-list keyed by token id until the access token expires.
About the Author
Celin Daniel is Co-founder of Coding Droplets with 13+ years of hands-on experience building, shipping, and operating .NET and ASP.NET Core systems in production. The guidance here comes from real projects and production incidents, not theory.
- Website: codingdroplets.com
- GitHub: github.com/codingdroplets
- YouTube: youtube.com/@CodingDroplets







