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:
Register a new user.
Log in and receive:
Access Token (JWT)
Refresh Token
Use the Access Token in Swagger's Authorize button.
Access protected endpoints.
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 JwtAuthDemoThis 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-NextThese 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 updateThis creates the database tables automatically.
Step 9 — Test in Swagger
Run the application.
Open
/swagger.Register a user.
Login and copy Access Token.
Click Authorize.
Paste:
Bearer <your_access_token>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.

Join the conversation! Your thoughts help the community grow.