ronnythedev/dotnet-clean-architecture-skills/12-dotnet-jwt-authentication

Claude Code and agent skill by ronnythedev/dotnet-clean-architecture-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Configures JWT Bearer authentication for .NET APIs. Includes token generation, validation, refresh tokens, and user context extraction from claims.

Install

npx vskill@latest install ronnythedev/dotnet-clean-architecture-skills/12-dotnet-jwt-authentication

SKILL.md

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

ComponentPurposeLocation
IJwtServiceToken generation interfaceApplication/Abstractions
JwtServiceToken generation implementationInfrastructure/Authentication
JwtOptionsconfiguration (expiration, issuer, etc.)Infrastructure/Authentication
JwtBearerOptionsSetupConfigure JWT validationInfrastructure/Authentication
IUserContextCurrent user infoApplication/Abstractions
UserContextExtract from HttpContextInfrastructure/Authentication
IRefreshTokenRepositoryRefresh token storageDomain/Identity
CookieSettingsCookie configurationInfrastructure/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

csharp
// 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
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

csharp
// 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

csharp
// 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

csharp
// 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 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<string> roles,
IEnumerable<string>? permissions,
IDictionary<string, string>? additionalClaims,
DateTime now,
DateTime expiration)
{
var claims = new List<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

csharp
// 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<JwtOptions> options)
{
_options = options.Value;
}

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
64
Repository forks
11
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/ronnythedev/dotnet-clean-architecture-skills/12-dotnet-jwt-authentication/badge)](https://verified-skill.com/skills/ronnythedev/dotnet-clean-architecture-skills/12-dotnet-jwt-authentication)

Something wrong with this skill or its page? Tell us.