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