Authentication is a critical part of any modern web application. In this article, we’ll build a secure JWT-based authentication system with Refresh Tokens using ASP.NET Core, Identity, and Dapper.
We’ll cover:
Login with JWT
Refresh token mechanism
Logout & token revocation
Change password with security stamp validation
📦 NuGet Packages Used
Install these packages:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Dapper
dotnet add package System.IdentityModel.Tokens.Jwt🏗️ Project Architecture
We are using a clean architecture approach:
API Layer (Controllers)
Application Layer (Interfaces)
Infrastructure Layer (Repositories, Services)
Persistence Layer (Dapper + EF Core)🔑 JWT Configuration (appsettings.json)
"Jwt": {
"Key": "THIS_IS_MY_SUPER_SECRET_KEY_123456789",
"Issuer": "ProductAPI",
"Audience": "ProductClient",
"AccessTokenMinutes": 10,
"RefreshTokenDays": 7
}⚙️ Configure JWT Authentication
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
opt.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var userManager = context.HttpContext.RequestServices
.GetRequiredService<UserManager<ApplicationUser>>();
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
var tokenStamp = context.Principal?.FindFirstValue("securityStamp");
var user = await userManager.FindByIdAsync(userId);
if (user == null || user.SecurityStamp != tokenStamp)
{
context.Fail("Token is no longer valid.");
}
}
};
});👉 This ensures tokens become invalid if the user logs out or changes password.
🔐 Login API
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null || !await _userManager.CheckPasswordAsync(user, request.Password))
return Unauthorized("Invalid credentials");
var roles = await _userManager.GetRolesAsync(user);
var role = roles.FirstOrDefault();
var accessToken = GenerateAccessToken(user, role);
var refreshToken = await CreateAndSaveRefreshTokenAsync(user.Id);
return Ok(new { accessToken, refreshToken = refreshToken.Token });
}🧾 Generate JWT Token
private string GenerateAccessToken(ApplicationUser user, string role)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.UserName ?? ""),
new Claim(ClaimTypes.Role, role),
new Claim("securityStamp", user.SecurityStamp ?? "")
};
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(10),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(token);
}🔄 Refresh Token API
[HttpPost("refresh")]
public async Task<IActionResult> Refresh(RefreshRequest request)
{
var stored = await _authUserRepository.GetRefreshTokenAsync(request.RefreshToken);
if (stored == null || stored.IsRevoked || stored.ExpiresOn < DateTime.UtcNow)
return Unauthorized("Invalid or expired refresh token");
var user = await _userManager.FindByIdAsync(stored.UserId);
await _authUserRepository.RevokeRefreshTokenAsync(stored.Token);
var newAccessToken = GenerateAccessToken(user, "User");
var newRefreshToken = await CreateAndSaveRefreshTokenAsync(user.Id);
return Ok(new
{
accessToken = newAccessToken,
refreshToken = newRefreshToken.Token
});
}🚪 Logout API




Join the conversation! Your thoughts help the community grow.