jwt-authentication

JWT Authentication Setup

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "jwt-authentication" with this command: npx skills add ronnythedev/dotnet-clean-architecture-skills/ronnythedev-dotnet-clean-architecture-skills-jwt-authentication

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";

/// &#x3C;summary>
/// Cookie domain (leave empty for current domain)
/// &#x3C;/summary>
public string? Domain { get; init; }

/// &#x3C;summary>
/// Cookie path
/// &#x3C;/summary>
public string Path { get; init; } = "/api/v1/auth";

/// &#x3C;summary>
/// SameSite policy (Strict recommended for healthcare)
/// &#x3C;/summary>
public SameSiteMode SameSite { get; init; } = SameSiteMode.Strict;

/// &#x3C;summary>
/// Require HTTPS (always true in production)
/// &#x3C;/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);

/// &#x3C;summary>
/// Generate tokens with custom claims
/// &#x3C;/summary>
TokenGenerationResult GenerateTokens(
    Guid userId,
    string email,
    IEnumerable&#x3C;string> roles,
    IDictionary&#x3C;string, string>? additionalClaims = null);

/// &#x3C;summary>
/// Hash a refresh token for secure database storage
/// &#x3C;/summary>
string HashRefreshToken(string refreshToken);

/// &#x3C;summary>
/// Verify a plain refresh token against its hash
/// &#x3C;/summary>
bool VerifyRefreshToken(string plainToken, string hashedToken);

/// &#x3C;summary>
/// Get access token expiration time
/// &#x3C;/summary>
DateTime GetAccessTokenExpiry();

/// &#x3C;summary>
/// Get refresh token expiration time
/// &#x3C;/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&#x3C;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&#x3C;string> roles,
    IEnumerable&#x3C;string>? permissions = null)
{
    var additionalClaims = new Dictionary&#x3C;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&#x3C;string> roles,
    IDictionary&#x3C;string, string>? additionalClaims = null)
{
    return GenerateTokensInternal(userId, email, roles, null, additionalClaims);
}

private TokenGenerationResult GenerateTokensInternal(
    Guid userId,
    string email,
    IEnumerable&#x3C;string> roles,
    IEnumerable&#x3C;string>? permissions,
    IDictionary&#x3C;string, string>? additionalClaims)
{
    var now = _dateTimeProvider.UtcNow;
    var accessTokenExpiration = now.AddMinutes(_options.AccessTokenExpirationMinutes);
    var refreshTokenExpiration = now.AddDays(_options.RefreshTokenExpirationDays);

    // Generate access token
    var accessToken = GenerateAccessToken(
        userId,
        email,
        roles,
        permissions,
        additionalClaims,
        now,
        accessTokenExpiration);

    // Generate opaque refresh token
    var refreshToken = GenerateRefreshToken();

    return new TokenGenerationResult(
        accessToken,
        refreshToken,
        accessTokenExpiration,
        refreshTokenExpiration);
}

private string GenerateAccessToken(
    Guid userId,
    string email,
    IEnumerable&#x3C;string> roles,
    IEnumerable&#x3C;string>? permissions,
    IDictionary&#x3C;string, string>? additionalClaims,
    DateTime now,
    DateTime expiration)
{
    var claims = new List&#x3C;Claim>
    {
        new(JwtRegisteredClaimNames.Sub, userId.ToString()),
        new(JwtRegisteredClaimNames.Email, email),
        new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new(JwtRegisteredClaimNames.Iat,
            new DateTimeOffset(now).ToUnixTimeSeconds().ToString(),
            ClaimValueTypes.Integer64)
    };

    // Add roles
    foreach (var role in roles)
    {
        claims.Add(new Claim(ClaimTypes.Role, role));
    }

    // Add permissions
    if (permissions is not null)
    {
        foreach (var permission in permissions)
        {
            claims.Add(new Claim("permission", permission));
        }
    }

    // Add additional claims
    if (additionalClaims is not null)
    {
        foreach (var (key, value) in additionalClaims)
        {
            if (!string.IsNullOrEmpty(value))
            {
                claims.Add(new Claim(key, value));
            }
        }
    }

    var token = new JwtSecurityToken(
        issuer: _options.Issuer,
        audience: _options.Audience,
        claims: claims,
        notBefore: now,
        expires: expiration,
        signingCredentials: _signingCredentials);

    return _tokenHandler.WriteToken(token);
}

private static string GenerateRefreshToken()
{
    var randomBytes = new byte[64];
    using var rng = RandomNumberGenerator.Create();
    rng.GetBytes(randomBytes);
    return Convert.ToBase64String(randomBytes);
}

public string HashRefreshToken(string refreshToken)
{
    var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(refreshToken));
    return Convert.ToHexString(bytes).ToLowerInvariant();
}

public bool VerifyRefreshToken(string plainToken, string hashedToken)
{
    var computedHash = HashRefreshToken(plainToken);
    return string.Equals(computedHash, hashedToken, StringComparison.OrdinalIgnoreCase);
}

public DateTime GetAccessTokenExpiry()
{
    return _dateTimeProvider.UtcNow.AddMinutes(_options.AccessTokenExpirationMinutes);
}

public DateTime GetRefreshTokenExpiry()
{
    return _dateTimeProvider.UtcNow.AddDays(_options.RefreshTokenExpirationDays);
}

}

Template: Refresh Token Cookie Manager

// src/{name}.infrastructure/Authentication/RefreshTokenCookieManager.cs using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options;

namespace {name}.infrastructure.authentication;

public interface IRefreshTokenCookieManager { void SetRefreshTokenCookie(HttpResponse response, string refreshToken, DateTime expiry); string? GetRefreshTokenFromCookie(HttpRequest request); void ClearRefreshTokenCookie(HttpResponse response); }

internal sealed class RefreshTokenCookieManager : IRefreshTokenCookieManager { private readonly JwtOptions _options;

public RefreshTokenCookieManager(IOptions&#x3C;JwtOptions> options)
{
    _options = options.Value;
}

public void SetRefreshTokenCookie(HttpResponse response, string refreshToken, DateTime expiry)
{
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,                          // Not accessible via JavaScript (XSS protection)
        Secure = _options.Cookie.SecureOnly,      // HTTPS only
        SameSite = _options.Cookie.SameSite,      // CSRF protection
        Expires = expiry,
        Path = _options.Cookie.Path,
        Domain = string.IsNullOrEmpty(_options.Cookie.Domain) 
            ? null 
            : _options.Cookie.Domain,
        IsEssential = true                        // Required for GDPR compliance
    };

    response.Cookies.Append(_options.Cookie.Name, refreshToken, cookieOptions);
}

public string? GetRefreshTokenFromCookie(HttpRequest request)
{
    return request.Cookies.TryGetValue(_options.Cookie.Name, out var token) 
        ? token 
        : null;
}

public void ClearRefreshTokenCookie(HttpResponse response)
{
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,
        Secure = _options.Cookie.SecureOnly,
        SameSite = _options.Cookie.SameSite,
        Expires = DateTime.UtcNow.AddDays(-1),    // Expire immediately
        Path = _options.Cookie.Path,
        Domain = string.IsNullOrEmpty(_options.Cookie.Domain) 
            ? null 
            : _options.Cookie.Domain
    };

    response.Cookies.Append(_options.Cookie.Name, string.Empty, cookieOptions);
}

}

Template: Refresh Token Entity

// src/{name}.domain/identity/RefreshToken.cs namespace {name}.domain.identity;

public sealed class RefreshToken { public Guid Id { get; private set; } public Guid UserId { get; private set; } public string TokenHash { get; private set; } = string.Empty; public DateTime CreatedAt { get; private set; } public DateTime ExpiresAt { get; private set; } public DateTime? RevokedAt { get; private set; } public string? ReplacedByTokenHash { get; private set; } public string? DeviceInfo { get; private set; } public string? IpAddress { get; private set; }

public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
public bool IsRevoked => RevokedAt.HasValue;
public bool IsActive => !IsRevoked &#x26;&#x26; !IsExpired;

private RefreshToken() { }

public static RefreshToken Create(
    Guid userId,
    string tokenHash,
    DateTime expiresAt,
    string? deviceInfo = null,
    string? ipAddress = null)
{
    return new RefreshToken
    {
        Id = Guid.NewGuid(),
        UserId = userId,
        TokenHash = tokenHash,
        CreatedAt = DateTime.UtcNow,
        ExpiresAt = expiresAt,
        DeviceInfo = deviceInfo,
        IpAddress = ipAddress
    };
}

public void Revoke(string? replacedByTokenHash = null)
{
    RevokedAt = DateTime.UtcNow;
    ReplacedByTokenHash = replacedByTokenHash;
}

}

Template: JWT Bearer Options Setup

// src/{name}.infrastructure/Authentication/JwtBearerOptionsSetup.cs using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens;

namespace {name}.infrastructure.authentication;

internal sealed class JwtBearerOptionsSetup : IConfigureNamedOptions<JwtBearerOptions> { private readonly JwtOptions _jwtOptions;

public JwtBearerOptionsSetup(IOptions&#x3C;JwtOptions> jwtOptions)
{
    _jwtOptions = jwtOptions.Value;
}

public void Configure(JwtBearerOptions options)
{
    Configure(JwtBearerDefaults.AuthenticationScheme, options);
}

public void Configure(string? name, JwtBearerOptions options)
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = _jwtOptions.Issuer,
        
        ValidateAudience = true,
        ValidAudience = _jwtOptions.Audience,
        
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_jwtOptions.SecretKey)),
        
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero,  // No tolerance for expiration
        
        // Ensure we get the user ID from the token
        NameClaimType = "sub",
        RoleClaimType = "role"
    };

    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            if (context.Exception is SecurityTokenExpiredException)
            {
                context.Response.Headers.Append(
                    "Token-Expired",
                    "true");
            }
            return Task.CompletedTask;
        },
        OnChallenge = context =>
        {
            // Custom response for 401
            return Task.CompletedTask;
        },
        OnForbidden = context =>
        {
            // Custom response for 403
            return Task.CompletedTask;
        }
    };
}

}

Template: User Context Interface

// src/{name}.application/Abstractions/Authentication/IUserContext.cs namespace {name}.application.abstractions.authentication;

public interface IUserContext { /// <summary> /// Current authenticated user's ID /// </summary> Guid UserId { get; }

/// &#x3C;summary>
/// Current user's email
/// &#x3C;/summary>
string Email { get; }

/// &#x3C;summary>
/// Current user's organization ID
/// &#x3C;/summary>
Guid? OrganizationId { get; }

/// &#x3C;summary>
/// Current user's roles
/// &#x3C;/summary>
IReadOnlyList&#x3C;string> Roles { get; }

/// &#x3C;summary>
/// Check if user is authenticated
/// &#x3C;/summary>
bool IsAuthenticated { get; }

/// &#x3C;summary>
/// Check if user has a specific role
/// &#x3C;/summary>
bool IsInRole(string role);

/// &#x3C;summary>
/// Get a custom claim value
/// &#x3C;/summary>
string? GetClaimValue(string claimType);

}

Template: User Context Implementation

// src/{name}.infrastructure/Authentication/UserContext.cs using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.AspNetCore.Http; using {name}.application.abstractions.authentication;

namespace {name}.infrastructure.authentication;

internal sealed class UserContext : IUserContext { private readonly IHttpContextAccessor _httpContextAccessor;

public UserContext(IHttpContextAccessor httpContextAccessor)
{
    _httpContextAccessor = httpContextAccessor;
}

private ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User;

public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false;

public Guid UserId
{
    get
    {
        var userIdClaim = User?.FindFirst(JwtRegisteredClaimNames.Sub)?.Value
            ?? User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        return Guid.TryParse(userIdClaim, out var userId)
            ? userId
            : throw new InvalidOperationException("User ID not found in claims");
    }
}

public string Email
{
    get
    {
        return User?.FindFirst(JwtRegisteredClaimNames.Email)?.Value
            ?? User?.FindFirst(ClaimTypes.Email)?.Value
            ?? string.Empty;
    }
}

public Guid? OrganizationId
{
    get
    {
        var orgIdClaim = User?.FindFirst("organization_id")?.Value;
        return Guid.TryParse(orgIdClaim, out var orgId) ? orgId : null;
    }
}

public IReadOnlyList&#x3C;string> Roles
{
    get
    {
        return User?.FindAll(ClaimTypes.Role)
            .Select(c => c.Value)
            .ToList()
            ?? new List&#x3C;string>();
    }
}

public bool IsInRole(string role)
{
    return User?.IsInRole(role) ?? false;
}

public string? GetClaimValue(string claimType)
{
    return User?.FindFirst(claimType)?.Value;
}

}

Template: Authentication Registration

// src/{name}.infrastructure/Authentication/AuthenticationExtensions.cs using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using {name}.application.abstractions.authentication;

namespace {name}.infrastructure.authentication;

public static class AuthenticationExtensions { public static IServiceCollection AddJwtAuthentication( this IServiceCollection services, IConfiguration configuration) { // Bind JWT options services.Configure<JwtOptions>( configuration.GetSection(JwtOptions.SectionName));

    // Register JWT Bearer authentication
    services
        .AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer();

    // Configure JWT Bearer options
    services.ConfigureOptions&#x3C;JwtBearerOptionsSetup>();

    // Register services
    services.AddHttpContextAccessor();
    services.AddScoped&#x3C;IJwtService, JwtService>();
    services.AddScoped&#x3C;IUserContext, UserContext>();

    return services;
}

}

Template: Login Command Handler

// src/{name}.application/Users/Login/LoginUserCommandHandler.cs using {name}.application.Abstractions.Authentication; using {name}.application.Abstractions.Messaging; using {name}.domain.abstractions; using {name}.domain.identity;

namespace {name}.application.users.login;

public sealed record LoginUserCommand( string Email, string Password, string? DeviceInfo = null, string? IpAddress = null) : ICommand<TokenResponse>;

internal sealed class LoginUserCommandHandler : ICommandHandler<LoginUserCommand, TokenResponse> { private readonly IUserRepository _userRepository; private readonly IPasswordHasher _passwordHasher; private readonly IJwtService _jwtService; private readonly IRoleRepository _roleRepository; private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly IUnitOfWork _unitOfWork;

public LoginUserCommandHandler(
    IUserRepository userRepository,
    IPasswordHasher passwordHasher,
    IJwtService jwtService,
    IRoleRepository roleRepository,
    IRefreshTokenRepository refreshTokenRepository,
    IUnitOfWork unitOfWork)
{
    _userRepository = userRepository;
    _passwordHasher = passwordHasher;
    _jwtService = jwtService;
    _roleRepository = roleRepository;
    _refreshTokenRepository = refreshTokenRepository;
    _unitOfWork = unitOfWork;
}

public async Task&#x3C;Result&#x3C;TokenResponse>> Handle(
    LoginUserCommand request,
    CancellationToken cancellationToken)
{
    // Find user by email
    var user = await _userRepository.GetByEmailAsync(
        request.Email,
        cancellationToken);

    if (user is null)
    {
        return Result.Failure&#x3C;TokenResponse>(UserErrors.InvalidCredentials);
    }

    // Verify password
    if (!_passwordHasher.Verify(request.Password, user.PasswordHash))
    {
        return Result.Failure&#x3C;TokenResponse>(UserErrors.InvalidCredentials);
    }

    // Check if user is active
    if (!user.IsActive)
    {
        return Result.Failure&#x3C;TokenResponse>(UserErrors.AccountDeactivated);
    }

    // Get user roles and permissions
    var roles = await _roleRepository.GetRolesByUserIdAsync(user.Id, cancellationToken);
    var roleNames = roles.Select(r => r.Name);
    var permissions = roles.SelectMany(r => r.Permissions).Distinct();

    // Generate tokens
    var tokenResult = _jwtService.GenerateTokens(user, roleNames, permissions);

    // Store hashed refresh token in database
    var refreshTokenEntity = RefreshToken.Create(
        userId: user.Id,
        tokenHash: _jwtService.HashRefreshToken(tokenResult.RefreshToken),
        expiresAt: tokenResult.RefreshTokenExpiration,
        deviceInfo: request.DeviceInfo,
        ipAddress: request.IpAddress);

    _refreshTokenRepository.Add(refreshTokenEntity);
    await _unitOfWork.SaveChangesAsync(cancellationToken);

    // Return access token (refresh token will be set in cookie by controller)
    return new TokenResponse(
        tokenResult.AccessToken,
        tokenResult.AccessTokenExpiration);
}

}

Template: Refresh Token Command Handler

// src/{name}.application/Users/RefreshToken/RefreshTokenCommandHandler.cs using {name}.application.Abstractions.Authentication; using {name}.application.Abstractions.Messaging; using {name}.domain.abstractions; using {name}.domain.identity;

namespace {name}.application.users.refreshToken;

public sealed record RefreshTokenCommand( string RefreshToken, string? DeviceInfo = null, string? IpAddress = null) : ICommand<TokenGenerationResult>;

internal sealed class RefreshTokenCommandHandler : ICommandHandler<RefreshTokenCommand, TokenGenerationResult> { private readonly IJwtService _jwtService; private readonly IUserRepository _userRepository; private readonly IRoleRepository _roleRepository; private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly IUnitOfWork _unitOfWork;

public RefreshTokenCommandHandler(
    IJwtService jwtService,
    IUserRepository userRepository,
    IRoleRepository roleRepository,
    IRefreshTokenRepository refreshTokenRepository,
    IUnitOfWork unitOfWork)
{
    _jwtService = jwtService;
    _userRepository = userRepository;
    _roleRepository = roleRepository;
    _refreshTokenRepository = refreshTokenRepository;
    _unitOfWork = unitOfWork;
}

public async Task&#x3C;Result&#x3C;TokenGenerationResult>> Handle(
    RefreshTokenCommand request,
    CancellationToken cancellationToken)
{
    // Hash the incoming token to find it in database
    var tokenHash = _jwtService.HashRefreshToken(request.RefreshToken);

    // Find the stored refresh token
    var storedToken = await _refreshTokenRepository.GetByHashAsync(
        tokenHash,
        cancellationToken);

    if (storedToken is null || !storedToken.IsActive)
    {
        return Result.Failure&#x3C;TokenGenerationResult>(UserErrors.InvalidRefreshToken);
    }

    // Get user
    var user = await _userRepository.GetByIdAsync(
        storedToken.UserId,
        cancellationToken);

    if (user is null || !user.IsActive)
    {
        // Revoke the token if user is invalid
        storedToken.Revoke();
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        return Result.Failure&#x3C;TokenGenerationResult>(UserErrors.InvalidRefreshToken);
    }

    // Get current roles and permissions
    var roles = await _roleRepository.GetRolesByUserIdAsync(user.Id, cancellationToken);
    var roleNames = roles.Select(r => r.Name);
    var permissions = roles.SelectMany(r => r.Permissions).Distinct();

    // Generate new tokens
    var tokenResult = _jwtService.GenerateTokens(user, roleNames, permissions);

    // Rotate refresh token: revoke old, create new
    var newTokenHash = _jwtService.HashRefreshToken(tokenResult.RefreshToken);
    storedToken.Revoke(replacedByTokenHash: newTokenHash);

    var newRefreshToken = Domain.Identity.RefreshToken.Create(
        userId: user.Id,
        tokenHash: newTokenHash,
        expiresAt: tokenResult.RefreshTokenExpiration,
        deviceInfo: request.DeviceInfo,
        ipAddress: request.IpAddress);

    _refreshTokenRepository.Add(newRefreshToken);
    await _unitOfWork.SaveChangesAsync(cancellationToken);

    return tokenResult;
}

}

Template: Auth Controller

// src/{name}.api/Controllers/Auth/AuthController.cs using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using {name}.application.abstractions.authentication; using {name}.application.users.login; using {name}.application.users.refreshToken; using {name}.infrastructure.authentication;

namespace {name}.api.Controllers.Auth;

[ApiController] [Route("api/v1/auth")] public class AuthController : ControllerBase { private readonly ISender _sender; private readonly IRefreshTokenCookieManager _cookieManager;

public AuthController(
    ISender sender,
    IRefreshTokenCookieManager cookieManager)
{
    _sender = sender;
    _cookieManager = cookieManager;
}

[HttpPost("login")]
[AllowAnonymous]
public async Task&#x3C;IActionResult> LoginFull(
    [FromBody] LoginRequest request,
    CancellationToken cancellationToken)
{
    var command = new LoginUserCommand(
        request.Email,
        request.Password,
        DeviceInfo: Request.Headers.UserAgent,
        IpAddress: HttpContext.Connection.RemoteIpAddress?.ToString());

    var result = await _sender.Send(command, cancellationToken);

    if (result.IsFailure)
    {
        return Unauthorized(new { error = result.Error.Code, message = result.Error.Message });
    }

    // Set refresh token in HttpOnly cookie
    _cookieManager.SetRefreshTokenCookie(
        Response,
        result.Value.RefreshToken,
        result.Value.RefreshTokenExpiration);

    // Return only access token in response body
    return Ok(new TokenResponse(
        result.Value.AccessToken,
        result.Value.AccessTokenExpiration));
}

[HttpPost("refresh")]
[AllowAnonymous]
public async Task&#x3C;IActionResult> RefreshToken(CancellationToken cancellationToken)
{
    // Get refresh token from HttpOnly cookie
    var refreshToken = _cookieManager.GetRefreshTokenFromCookie(Request);

    if (string.IsNullOrEmpty(refreshToken))
    {
        return Unauthorized(new { error = "invalid_token", message = "Refresh token not found" });
    }

    var command = new RefreshTokenCommand(
        refreshToken,
        DeviceInfo: Request.Headers.UserAgent,
        IpAddress: HttpContext.Connection.RemoteIpAddress?.ToString());

    var result = await _sender.Send(command, cancellationToken);

    if (result.IsFailure)
    {
        // Clear invalid cookie
        _cookieManager.ClearRefreshTokenCookie(Response);
        return Unauthorized(new { error = result.Error.Code, message = result.Error.Message });
    }

    // Set new refresh token in HttpOnly cookie (rotation)
    _cookieManager.SetRefreshTokenCookie(
        Response,
        result.Value.RefreshToken,
        result.Value.RefreshTokenExpiration);

    // Return only access token in response body
    return Ok(new TokenResponse(
        result.Value.AccessToken,
        result.Value.AccessTokenExpiration));
}

[HttpPost("logout")]
[Authorize]
public async Task&#x3C;IActionResult> Logout(CancellationToken cancellationToken)
{
    // Get refresh token from cookie and revoke it
    var refreshToken = _cookieManager.GetRefreshTokenFromCookie(Request);

    if (!string.IsNullOrEmpty(refreshToken))
    {
        var command = new RevokeRefreshTokenCommand(refreshToken);
        await _sender.Send(command, cancellationToken);
    }

    // Clear the cookie
    _cookieManager.ClearRefreshTokenCookie(Response);

    return NoContent();
}

[HttpGet("me")]
[Authorize]
public IActionResult GetCurrentUser([FromServices] IUserContext userContext)
{
    return Ok(new
    {
        UserId = userContext.UserId,
        Email = userContext.Email,
        Name = userContext.Name,
        PatientId = userContext.PatientId,
        Roles = userContext.Roles,
        Permissions = userContext.Permissions
    });
}

}

public sealed record LoginRequest(string Email, string Password);

Template: Dependency Injection Registration

// src/{name}.infrastructure/DependencyInjection.cs private static void AddAuthentication(IServiceCollection services, IConfiguration configuration) { // Configure JWT options with nested cookie settings services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));

// Register authentication services
services.AddScoped&#x3C;IJwtService, JwtService>();
services.AddScoped&#x3C;IRefreshTokenCookieManager, RefreshTokenCookieManager>();

// Register HttpContextAccessor for UserContext
services.AddHttpContextAccessor();
services.AddScoped&#x3C;IUserContext, UserContext>();

// Configure JWT Bearer authentication
services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer();

services.ConfigureOptions&#x3C;JwtBearerOptionsSetup>();

// Configure authorization
services.AddAuthorization();

}

Critical Rules

  • Secret key length - At least 32 characters for HMAC-SHA256

  • Store secrets securely - Use Azure Key Vault, AWS Secrets, etc.

  • Short access tokens - 15-60 minutes typical

  • Longer refresh tokens - 7-30 days typical

  • Validate all claims - Issuer, audience, signature, expiration

  • No clock skew - Set ClockSkew = TimeSpan.Zero

  • HTTPS only - Never transmit tokens over HTTP

  • HttpOnly cookies - Refresh tokens should never be accessible via JavaScript

  • Token rotation - Issue new refresh token on each use

  • Revoke on logout - Always revoke refresh token on logout

  • Use IUserContext - Don't access HttpContext directly in handlers

Anti-Patterns to Avoid

// ❌ WRONG: Returning refresh token in response body return Ok(new { accessToken, refreshToken }); // Exposed to XSS!

// ✅ CORRECT: Set refresh token in HttpOnly cookie _cookieManager.SetRefreshTokenCookie(Response, refreshToken, expiry); return Ok(new { accessToken });

// ❌ WRONG: Storing plain refresh token await _db.RefreshTokens.AddAsync(new { Token = refreshToken });

// ✅ CORRECT: Store hashed token await _db.RefreshTokens.AddAsync(new { TokenHash = _jwtService.HashRefreshToken(refreshToken) });

// ❌ WRONG: Short secret key "SecretKey": "abc123" // Too short, insecure!

// ✅ CORRECT: Strong secret key "SecretKey": "your-secret-key-at-least-32-characters-long-for-security"

// ❌ WRONG: Accessing HttpContext in handler public class Handler { private readonly IHttpContextAccessor _accessor; var userId = _accessor.HttpContext.User.FindFirst("sub"); // Don't! }

// ✅ CORRECT: Use IUserContext abstraction public class Handler { private readonly IUserContext _userContext; var userId = _userContext.UserId; }

// ❌ WRONG: Never expiring refresh tokens ExpiresAt = DateTime.MaxValue // Security risk!

// ✅ CORRECT: Configured expiration ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenExpirationDays)

// ❌ WRONG: Never expiring tokens expires: DateTime.MaxValue // Security risk!

// ✅ CORRECT: Short-lived tokens with refresh expires: DateTime.UtcNow.AddMinutes(60)

Related Skills

  • permission-authorization

  • Permission-based access control

  • api-controller-generator

  • Protected API endpoints

  • dotnet-clean-architecture

  • Infrastructure layer setup

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

dotnet-clean-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

unit-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

quartz-background-jobs

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

dapper-query-builder

No summary provided by upstream source.

Repository SourceNeeds Review