ASP.NET Core  

JWT Authentication & Authorization with Refresh Tokens in .NET 8

Introduction

In modern web applications, secure authentication and authorization are critical. This document provides a complete, practical implementation of JWT (JSON Web Token) Authentication and Authorization with Refresh Tokens using .NET 8, EF Core (Code-First), SQL Server, and Swagger.

The purpose of this guide is to help developers implement a production-ready authentication system by following clear, structured steps. Rather than focusing heavily on theory, this article emphasizes hands-on implementation so that readers can directly build and test the solution.

By the end of this guide, you will have:

  • A working JWT authentication system

  • Short-lived access tokens

  • Secure refresh token mechanism

  • Role-based authorization

  • Database integration using EF Core

  • Swagger integration for easy testing

Expected Output

After completing all steps, you will be able to:

  1. Register a new user.

  2. Log in and receive:

    • Access Token (JWT)

    • Refresh Token

  3. Use the Access Token in Swagger's Authorize button.

  4. Access protected endpoints.

  5. Generate a new Access Token using the Refresh Token when the original expires.

Sample Login Response Output

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "d3f6c2a1-5f3a-4a8e-b5e2-9c4b7f8e6a2d"
}

Sample Protected API Output

"Welcome to Dashboard (Authenticated User)"

Step-by-Step Implementation

Step 1 — Create the Project

dotnet new webapi -n JwtAuthDemo
cd JwtAuthDemo

This creates a new .NET 8 Web API project.

Step 2 — Install Required Packages

dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.7
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.7
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.7
dotnet add package Swashbuckle.AspNetCore --version 6.6.2
dotnet add package BCrypt.Net-Next

These packages enable:

  • Database access

  • JWT authentication

  • Swagger integration

  • Secure password hashing

Step 3 — Create Models

User Model

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string PasswordHash { get; set; }
    public string Role { get; set; } = "User";
    public List<RefreshToken> RefreshTokens { get; set; } = new();
}

Refresh Token Model

public class RefreshToken
{
    public int Id { get; set; }
    public string Token { get; set; } = Guid.NewGuid().ToString();
    public DateTime Expires { get; set; }
    public bool IsRevoked { get; set; }
    public int UserId { get; set; }
    public User User { get; set; }
}

These models represent authentication data stored in the database.

Step 4 — Configure DbContext

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<User> Users => Set<User>();
    public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
}

This connects your models to the database.

Step 5 — Configure JWT in appsettings.json

"Jwt": {
  "Key": "THIS_IS_ULTRA_SECRET_KEY_123456",
  "Issuer": "JwtAuthDemo",
  "Audience": "JwtAuthDemoUsers",
  "AccessTokenMinutes": 15,
  "RefreshTokenDays": 7
}

Important: The Key must be at least 32 characters long to avoid HS256 errors.

Step 6 — Implement Token Service

This service generates access and refresh tokens.

public class TokenService
{
    private readonly IConfiguration _config;

    public TokenService(IConfiguration config)
    {
        _config = config;
    }

    public string CreateAccessToken(User user)
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, user.Username),
            new Claim(ClaimTypes.Role, user.Role)
        };

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:Key"]));

        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(
                int.Parse(_config["Jwt:AccessTokenMinutes"])),
            signingCredentials: new SigningCredentials(
                key, SecurityAlgorithms.HmacSha256)
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public RefreshToken CreateRefreshToken()
    {
        return new RefreshToken
        {
            Expires = DateTime.UtcNow.AddDays(
                int.Parse(_config["Jwt:RefreshTokenDays"]))
        };
    }
}

Step 7 — Implement Auth Controller

Handles Register, Login, and Refresh.

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly TokenService _tokenService;

    public AuthController(AppDbContext context, TokenService tokenService)
    {
        _context = context;
        _tokenService = tokenService;
    }

    [HttpPost("register")]
    public IActionResult Register(string username, string password)
    {
        var user = new User
        {
            Username = username,
            PasswordHash = BCrypt.Net.BCrypt.HashPassword(password)
        };

        _context.Users.Add(user);
        _context.SaveChanges();

        return Ok("User Created");
    }

    [HttpPost("login")]
    public IActionResult Login(string username, string password)
    {
        var user = _context.Users.FirstOrDefault(x => x.Username == username);

        if (user == null || !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
            return Unauthorized();

        var accessToken = _tokenService.CreateAccessToken(user);
        var refreshToken = _tokenService.CreateRefreshToken();

        user.RefreshTokens.Add(refreshToken);
        _context.SaveChanges();

        return Ok(new { accessToken, refreshToken = refreshToken.Token });
    }
}

Step 8 — Run Migration

dotnet ef migrations add InitialCreate
dotnet ef database update

This creates the database tables automatically.

Step 9 — Test in Swagger

  1. Run the application.

  2. Open /swagger.

  3. Register a user.

  4. Login and copy Access Token.

  5. Click Authorize.

  6. Paste:

Bearer <your_access_token>
  1. Access protected endpoints.

Conclusion

This implementation demonstrates a complete, real-world authentication system using .NET 8 and JWT with refresh tokens. By following the step-by-step instructions provided in this guide, developers can quickly build a secure and scalable authentication foundation.

Key Takeaways:

  • JWT enables stateless authentication.

  • Refresh tokens improve security and user experience.

  • EF Core simplifies database integration.

  • BCrypt ensures secure password storage.

  • Swagger allows easy API testing.