JWT Authentication Setup
Overview
This skill implements JWT (JSON Web Token) authentication for .NET APIs:
- Access Token - Short-lived JWT returned in response body
- Refresh Token - Stored in HttpOnly cookie (secure, not accessible via JavaScript)
- Options Pattern - Configurable expiration via JwtOptions
- Token Rotation - New refresh token issued on each refresh
- Security Audit - Comprehensive event tracking for compliance
- Token generation - Create access and refresh tokens
- Token validation - Validate incoming tokens
- User context - Extract user info from claims
Quick Reference
| Component | Purpose | Location |
|---|---|---|
IJwtService | Token generation interface | Application/Abstractions |
JwtService | Token generation implementation | Infrastructure/Authentication |
JwtOptions | configuration (expiration, issuer, etc.) | Infrastructure/Authentication |
JwtBearerOptionsSetup | Configure JWT validation | Infrastructure/Authentication |
IUserContext | Current user info | Application/Abstractions |
UserContext | Extract from HttpContext | Infrastructure/Authentication |
IRefreshTokenRepository | Refresh token storage | Domain/Identity |
CookieSettings | Cookie configuration | Infrastructure/Authentication |
Authentication Structure
/Application/Abstractions/
├── Authentication/
│ ├── IJwtService.cs
│ ├── IUserContext.cs
│ ├── TokenResponse.cs
│ └── AuthenticationErrors.cs
/Infrastructure/
├── Authentication/
│ ├── JwtOptions.cs
│ ├── JwtService.cs
│ ├── JwtBearerOptionsSetup.cs
│ ├── UserContext.cs
│ ├── CookieSettings.cs
│ └── RefreshTokenCookieManager.cs
Template: JWT Configuration Options
// src/{name}.infrastructure/Authentication/JwtOptions.cs
namespace {name}.infrastructure.authentication;
public sealed class JwtOptions
{
public const string SectionName = "Jwt";
public string Issuer { get; init; } = string.Empty;
public string Audience { get; init; } = string.Empty;
public string SecretKey { get; init; } = string.Empty;
public int AccessTokenExpirationMinutes { get; init; } = 60;
public int RefreshTokenExpirationDays { get; init; } = 7;
public CookieSettings Cookie { get; init; } = new();
}
public sealed class CookieSettings
{
/// <summary>
/// Name of the refresh token cookie
/// </summary>
public string Name { get; init; } = "X-Refresh-Token";
/// <summary>
/// Cookie domain (leave empty for current domain)
/// </summary>
public string? Domain { get; init; }
/// <summary>
/// Cookie path
/// </summary>
public string Path { get; init; } = "/api/v1/auth";
/// <summary>
/// SameSite policy (Strict recommended for healthcare)
/// </summary>
public SameSiteMode SameSite { get; init; } = SameSiteMode.Strict;
/// <summary>
/// Require HTTPS (always true in production)
/// </summary>
public bool SecureOnly { get; init; } = true;
}
appsettings.json
{
"Jwt": {
"Issuer": "your-app-name",
"Audience": "your-app-name",
"SecretKey": "your-secret-key-at-least-32-characters-long-for-security",
"AccessTokenExpirationMinutes": 60,
"RefreshTokenExpirationDays": 7,
"Cookie": {
"Name": "X-Refresh-Token",
"Domain": "",
"Path": "/api/v1/auth",
"SameSite": "Strict",
"SecureOnly": true
}
}
}
Template: JWT Service Interface
// src/{name}.application/Abstractions/Authentication/IJwtService.cs
using {name}.domain.users;
namespace {name}.application.abstractions.authentication;
public interface IJwtService
{
/// <summary>
/// Generate access and refresh tokens for a user
/// </summary>
TokenGenerationResult GenerateTokens(
User user,
IEnumerable<string> roles,
IEnumerable<string>? permissions = null);
/// <summary>
/// Generate tokens with custom claims
/// </summary>
TokenGenerationResult GenerateTokens(
Guid userId,
string email,
IEnumerable<string> roles,
IDictionary<string, string>? additionalClaims = null);
/// <summary>
/// Hash a refresh token for secure database storage
/// </summary>
string HashRefreshToken(string refreshToken);
/// <summary>
/// Verify a plain refresh token against its hash
/// </summary>
bool VerifyRefreshToken(string plainToken, string hashedToken);
/// <summary>
/// Get access token expiration time
/// </summary>
DateTime GetAccessTokenExpiry();
/// <summary>
/// Get refresh token expiration time
/// </summary>
DateTime GetRefreshTokenExpiry();
}
Template: Token Response
// src/{name}.application/Abstractions/Authentication/TokenResponse.cs
namespace {name}.application.abstractions.authentication;
/// <summary>
/// Response containing access token (refresh token is set via HttpOnly cookie)
/// </summary>
public sealed record TokenResponse(
string AccessToken,
DateTime AccessTokenExpiration,
string TokenType = "Bearer");
/// <summary>
/// Internal response including refresh token (for cookie setting)
/// </summary>
public sealed record TokenGenerationResult(
string AccessToken,
string RefreshToken,
DateTime AccessTokenExpiration,
DateTime RefreshTokenExpiration);
Template: JWT Service Implementation
// src/{name}.infrastructure/Authentication/JwtService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using {name}.application.Abstractions.Authentication;
using {name}.application.Abstractions.Clock;
using {name}.domain.identity;
namespace {name}.infrastructure.authentication;
internal sealed class JwtService : IJwtService
{
private readonly JwtOptions _options;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly SigningCredentials _signingCredentials;
private readonly JwtSecurityTokenHandler _tokenHandler;
public JwtService(
IOptions<JwtOptions> options,
IDateTimeProvider dateTimeProvider)
{
_options = options.Value;
_dateTimeProvider = dateTimeProvider;
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_options.SecretKey));
_signingCredentials = new SigningCredentials(
securityKey,
SecurityAlgorithms.HmacSha256);
_tokenHandler = new JwtSecurityTokenHandler();
}
public TokenGenerationResult GenerateTokens(
User user,
IEnumerable<string> roles,
IEnumerable<string>? permissions = null)
{
var additionalClaims = new Dictionary<string, string>
{
["name"] = $"{user.FirstName} {user.LastName}".Trim()
};
return GenerateTokensInternal(
user.Id,
user.Email,
roles,
permissions,
additionalClaims);
}
public TokenGenerationResult GenerateTokens(
Guid userId,
string email,
IEnumerable<string> roles,
IDictionary<string, string>? additionalClaims = null)
{
return GenerateTokensInternal(userId, email, roles, null, additionalClaims);
}
private TokenGenerationResult GenerateTokensInternal(
Guid userId,
string email,
IEnumerable<string> roles,
IEnumerable<string>? permissions,
IDictionary<string, string>? additionalClaims)
{
var now = _dateTimeProvider.UtcNow;
var accessTokenExpiration = now.AddMinutes(_options.AccessTokenExpirationMinutes);
var refreshTokenExpiration = now.AddDays(_options.RefreshTokenExpirationDays);
// Generate access token
var accessToke