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:
π¦ 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
[Authorize]
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
await _authUserRepository.RevokeAllUserRefreshTokensAsync(userId);
await _userManager.UpdateSecurityStampAsync(user);
return Ok("Logged out successfully");
}
π This invalidates all tokens instantly.
π Change Password API
[Authorize]
[HttpPost("change-password")]
public async Task<IActionResult> ChangePassword(ChangePasswordRequest request)
{
var user = await _userManager.FindByIdAsync(userId);
var result = await _userManager.ChangePasswordAsync(
user,
request.CurrentPassword,
request.NewPassword
);
await _authUserRepository.RevokeAllUserRefreshTokensAsync(userId);
await _userManager.UpdateSecurityStampAsync(user);
return Ok("Password changed successfully");
}
π Refresh Token Implementation with Dapper & SQL Server in ASP.NET Core
In the previous section, we implemented JWT authentication. Now letβs complete the system by adding a Refresh Token mechanism using SQL Server and Dapper.
ποΈ Database Table for Refresh Tokens
CREATE TABLE [dbo].[RefreshTokens]
(
Id INT IDENTITY(1,1) PRIMARY KEY,
UserId NVARCHAR(450) NOT NULL,
Token NVARCHAR(200) NOT NULL,
ExpiresOn DATETIME NOT NULL,
IsRevoked BIT NOT NULL DEFAULT 0,
CreatedOn DATETIME NOT NULL DEFAULT GETUTCDATE()
)
βοΈ Stored Procedures
β
Save Refresh Token
CREATE PROCEDURE [dbo].[SaveRefreshToken]
@UserId NVARCHAR(450),
@Token NVARCHAR(200),
@ExpiresOn DATETIME2
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO RefreshTokens (UserId, Token, ExpiresOn, IsRevoked, CreatedOn)
VALUES (@UserId, @Token, @ExpiresOn, 0, GETUTCDATE());
END
π Get Refresh Token
CREATE PROCEDURE [dbo].[GetRefreshToken]
@Token NVARCHAR(200)
AS
BEGIN
SET NOCOUNT ON;
SELECT Id, UserId, Token, ExpiresOn, IsRevoked, CreatedOn
FROM RefreshTokens
WHERE Token = @Token
AND IsRevoked = 0;
END
β Revoke Single Token
CREATE PROCEDURE [dbo].[RevokeRefreshToken]
@Token NVARCHAR(200)
AS
BEGIN
SET NOCOUNT ON;
UPDATE RefreshTokens
SET IsRevoked = 1
WHERE Token = @Token;
END
π« Revoke All Tokens for User
CREATE PROCEDURE [dbo].[RevokeAllUserRefreshTokens]
@UserId NVARCHAR(450)
AS
BEGIN
SET NOCOUNT ON;
UPDATE RefreshTokens
SET IsRevoked = 1
WHERE UserId = @UserId
AND IsRevoked = 0;
END
π§Ή Cleanup Expired Tokens (Optional but Recommended)
CREATE PROCEDURE [dbo].[CleanupExpiredRefreshTokens]
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM RefreshTokens
WHERE ExpiresOn < GETUTCDATE()
OR IsRevoked = 1;
END
π You can schedule this using Hangfire / SQL Job.
π Repository Interface
public interface IAuthUserRepository
{
Task SaveRefreshTokenAsync(RefreshToken token);
Task<RefreshToken?> GetRefreshTokenAsync(string token);
Task RevokeRefreshTokenAsync(string token);
Task RevokeAllUserRefreshTokensAsync(string userId);
}
β‘ Dapper Repository Implementation
public class AuthUserRepository : IAuthUserRepository
{
private readonly DapperContext _context;
public AuthUserRepository(DapperContext context)
{
_context = context;
}
public async Task SaveRefreshTokenAsync(RefreshToken token)
{
using var conn = _context.CreateConnection();
await conn.ExecuteAsync("SaveRefreshToken", new
{
token.UserId,
token.Token,
token.ExpiresOn
}, commandType: CommandType.StoredProcedure);
}
public async Task<RefreshToken?> GetRefreshTokenAsync(string token)
{
using var conn = _context.CreateConnection();
return await conn.QueryFirstOrDefaultAsync<RefreshToken>(
"GetRefreshToken",
new { Token = token },
commandType: CommandType.StoredProcedure);
}
public async Task RevokeRefreshTokenAsync(string token)
{
using var conn = _context.CreateConnection();
await conn.ExecuteAsync(
"RevokeRefreshToken",
new { Token = token },
commandType: CommandType.StoredProcedure);
}
public async Task RevokeAllUserRefreshTokensAsync(string userId)
{
using var conn = _context.CreateConnection();
await conn.ExecuteAsync(
"RevokeAllUserRefreshTokens",
new { UserId = userId },
commandType: CommandType.StoredProcedure);
}
}
![Table]()
![swagger]()
![table 2]()
π How It Works (Flow)
π’ Login
π Refresh Token
πͺ Logout
π Password Change