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;
}
}
}
Join the conversation! Your thoughts help the community grow.