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

Coding DropletsCoding Droplets
5 min read

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.

💡
Full source code is available on our Patreon. Get access and follow along!

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

🛠 Setting Up ASP.NET Core for JWT

To start using ASP.NET Core JWT, install these essential libraries:

  • System.IdentityModel.Tokens.Jwt

  • Microsoft.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, RefreshToken

  • Repositories: Encapsulate CRUD operations

  • DbContext: Includes DbSet<User> and DbSet<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.

💡
🔗 Check out the Source Code and implementation guide on Patreon. Includes everything you need to build a production-ready dot net API.

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

0
Subscribe to my newsletter

Read articles from Coding Droplets directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Coding Droplets
Coding Droplets