JSON Web Tokens (JWT) are the industry standard for stateless API authentication. However, because access tokens have a short lifespan for security reasons, modern applications require a Refresh Token mechanism to issue new access tokens without forcing the user to re-authenticate continuously.
Step 1: Install Required NuGet Packages
Add the necessary JWT bearer authentication package to your project:
Bash
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Step 2: Configure JWT Settings in appsettings.json
Store your signing keys, issuers, and token expiration lifetimes securely in configuration files.
JSON
{
"JwtSettings": {
"Secret": "SuperSecretKeyForJwtAuthenticationMustBeLongEnough123!",
"Issuer": "YourApiIssuer",
"Audience": "YourApiAudience",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7
}
}
Step 3: Create a Token Generation Service
Implement a service responsible for generating cryptographic access tokens and secure, cryptographically random refresh tokens.
C#
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
public interface ITokenService
{
string GenerateAccessToken(IEnumerable<Claim> claims);
string GenerateRefreshToken();
ClaimsPrincipal? GetPrincipalFromExpiredToken(string token);
}
public class TokenService : ITokenService
{
private readonly IConfiguration _configuration;
public TokenService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var key = Encoding.UTF8.GetBytes(_configuration["JwtSettings:Secret"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(int.Parse(_configuration["JwtSettings:AccessTokenExpirationMinutes"]!)),
Issuer = _configuration["JwtSettings:Issuer"],
Audience = _configuration["JwtSettings:Audience"],
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
public ClaimsPrincipal? GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = _configuration["JwtSettings:Audience"],
ValidateIssuer = true,
ValidIssuer = _configuration["JwtSettings:Issuer"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSettings:Secret"]!)),
ValidateLifetime = false // Crucial: allows parsing expired tokens during refresh flow
};
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
{
return null;
}
return principal;
}
catch
{
return null;
}
}
}
Step 4: Configure Authentication in Program.cs
Wire up JWT bearer authentication services into your dependency injection pipeline.
C#
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// Configure JWT Authentication
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey = Encoding.UTF8.GetBytes(jwtSettings["Secret"]!);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 5: Implement the Auth Controller
Create endpoints for user login (issuing both access and refresh tokens) and token refreshing.
C#
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly ITokenService _tokenService;
public AuthController(ITokenService tokenService)
{
_tokenService = tokenService;
}
[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel model)
{
// Validate user credentials against database (mocked here)
if (model.Username != "admin" || model.Password != "password")
return Unauthorized("Invalid credentials.");
var claims = new[] { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, model.Username) };
var accessToken = _tokenService.GenerateAccessToken(claims);
var refreshToken = _tokenService.GenerateRefreshToken();
// Save refresh token securely to database associated with the user...
return Ok(new { AccessToken = accessToken, RefreshToken = refreshToken });
}
[HttpPost("refresh")]
public IActionResult Refresh([FromBody] TokenRequestModel model)
{
var principal = _tokenService.GetPrincipalFromExpiredToken(model.AccessToken);
if (principal is null) return BadRequest("Invalid access token.");
var username = principal.Identity?.Name;
// Retrieve and validate stored refresh token from database for the user...
var newAccessToken = _tokenService.GenerateAccessToken(principal.Claims);
var newRefreshToken = _tokenService.GenerateRefreshToken();
return Ok(new { AccessToken = newAccessToken, RefreshToken = newRefreshToken });
}
}
public record LoginModel(string Username, string Password);
public record TokenRequestModel(string AccessToken, string RefreshToken);