
What We'll Build
We'll create:
User registration
Secure login
JWT access token generation
Refresh token generation
Token refresh endpoint
Logout / token revocation
Protected API endpoints
Role-based authorization
Security hardening
Tech stack:
ASP.NET Core 8/9 style APIs
Entity Framework Core
SQL Server
ASP.NET Identity password hashing
JWT Bearer Authentication
JWT Architecture Overview
Flow:
User Login
↓
Validate Credentials
↓
Generate Access Token (Short-Lived)
↓
Generate Refresh Token (Long-Lived)
↓
Return Tokens to Client
↓
Client Calls Protected APIs
↓
Access Token Expires
↓
Client Sends Refresh Token
↓
Server Validates Refresh Token
↓
Issue New Access Token
Project Setup
Create project:
dotnet new webapi -n JwtAuthDemo
cd JwtAuthDemo
Install packages:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Identity
Project Structure
JwtAuthDemo
┣ Controllers
┃ ┗ AuthController.cs
┣ Data
┃ ┗ AppDbContext.cs
┣ Models
┃ ┣ User.cs
┃ ┣ RefreshToken.cs
┃ ┣ LoginRequest.cs
┃ ┗ RegisterRequest.cs
┣ Services
┃ ┣ ITokenService.cs
┃ ┗ TokenService.cs
┣ appsettings.json
┗ Program.cs
Step 1: Create Domain Models
User Entity
namespace JwtAuthDemo.Models;
public class User
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Email { get; set; } = default!;
public string PasswordHash { get; set; } = default!;
public string Role { get; set; } = "User";
public ICollection<RefreshToken> RefreshTokens { get; set; }
= new List<RefreshToken>();
}
Refresh Token Entity
Never store refresh tokens in plain text.
Store hashed values.
namespace JwtAuthDemo.Models;
public class RefreshToken
{
public Guid Id { get; set; } = Guid.NewGuid();
public string TokenHash { get; set; } = default!;
public DateTime ExpiresAtUtc { get; set; }
public bool IsRevoked { get; set; }
public DateTime CreatedAtUtc { get; set; }
public Guid UserId { get; set; }
public User User { get; set; } = default!;
}
Step 2: DTOs
Register Request
namespace JwtAuthDemo.Models;
public class RegisterRequest
{
public string Email { get; set; } = default!;
public string Password { get; set; } = default!;
}
Login Request
namespace JwtAuthDemo.Models;
public class LoginRequest
{
public string Email { get; set; } = default!;
public string Password { get; set; } = default!;
}
Token Response
namespace JwtAuthDemo.Models;
public class TokenResponse
{
public string AccessToken { get; set; } = default!;
public string RefreshToken { get; set; } = default!;
}
Step 3: Database Context
using JwtAuthDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace JwtAuthDemo.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<User> Users => Set<User>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasIndex(x => x.Email)
.IsUnique();
base.OnModelCreating(modelBuilder);
}
}
Step 4: JWT Configuration
appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=JwtAuthDb;Trusted_Connection=True;TrustServerCertificate=True"
},
"Jwt": {
"Issuer": "JwtAuthDemo",
"Audience": "JwtAuthDemoUsers",
"SecretKey": "THIS_IS_FOR_DEV_ONLY_REPLACE_IN_PRODUCTION",
"AccessTokenMinutes": 15,
"RefreshTokenDays": 7
}
}
Production Security Warning
Never store secrets in appsettings.json in production.
Use:
Azure Key Vault
AWS Secrets Manager
Docker secrets
Kubernetes secrets
environment variables
Example:
builder.Configuration["Jwt:SecretKey"];
Step 5: Token Service
Interface
using JwtAuthDemo.Models;
namespace JwtAuthDemo.Services;
public interface ITokenService
{
string GenerateAccessToken(User user);
string GenerateRefreshToken();
string HashToken(string token);
}
Implementation
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using JwtAuthDemo.Models;
using Microsoft.IdentityModel.Tokens;
namespace JwtAuthDemo.Services;
public class TokenService : ITokenService
{
private readonly IConfiguration _configuration;
public TokenService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateAccessToken(User user)
{
var jwtSection = _configuration.GetSection("Jwt");
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSection["SecretKey"]!));
var credentials = new SigningCredentials(
key,
SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var expires = DateTime.UtcNow.AddMinutes(
int.Parse(jwtSection["AccessTokenMinutes"]!));
var token = new JwtSecurityToken(
issuer: jwtSection["Issuer"],
audience: jwtSection["Audience"],
claims: claims,
expires: expires,
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
var bytes = RandomNumberGenerator.GetBytes(64);
return Convert.ToBase64String(bytes);
}
public string HashToken(string token)
{
using var sha = SHA256.Create();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(token));
return Convert.ToBase64String(hash);
}
}
Step 6: Password Hashing
Never store passwords directly.
Use ASP.NET Identity's password hasher.
using Microsoft.AspNetCore.Identity;
var passwordHasher = new PasswordHasher<User>();
Registration:
var user = new User
{
Email = request.Email
};
user.PasswordHash = passwordHasher.HashPassword(
user,
request.Password);
Verification:
var result = passwordHasher.VerifyHashedPassword(
user,
user.PasswordHash,
request.Password);
Step 7: Configure Authentication
Program.cs:
using System.Text;
using JwtAuthDemo.Data;
using JwtAuthDemo.Models;
using JwtAuthDemo.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
var jwt = builder.Configuration.GetSection("Jwt");
var key = Encoding.UTF8.GetBytes(jwt["SecretKey"]!);
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = jwt["Issuer"],
ValidAudience = jwt["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Join the conversation! Your thoughts help the community grow.