skills/security/auth-jwt/SKILL.md
Use when setting up JWT authentication, token generation, or refresh token flows.
npx skillsauth add faysilalshareef/dotnet-ai-kit auth-jwtInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
JsonWebTokenHandler (.NET 10+) or JwtSecurityTokenHandler for token generation// Program.cs
builder.Services.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:Key"]!)),
ClockSkew = TimeSpan.FromSeconds(30) // default is 5 min
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
public sealed class JwtOptions
{
public const string SectionName = "Jwt";
[Required]
public required string Issuer { get; init; }
[Required]
public required string Audience { get; init; }
[Required, MinLength(32)]
public required string Key { get; init; }
[Range(1, 1440)]
public int AccessTokenExpiryMinutes { get; init; } = 60;
[Range(1, 43200)]
public int RefreshTokenExpiryMinutes { get; init; } = 10080; // 7 days
}
// Registration
builder.Services.AddOptions<JwtOptions>()
.BindConfiguration(JwtOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
public interface ITokenService
{
TokenResponse GenerateTokens(
User user, IEnumerable<string> permissions);
}
public sealed class TokenService(IOptions<JwtOptions> options)
: ITokenService
{
public TokenResponse GenerateTokens(
User user, IEnumerable<string> permissions)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(JwtRegisteredClaimNames.Email, user.Email),
new(JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString()),
new("permissions", string.Join(",", permissions))
};
foreach (var role in user.Roles)
claims.Add(new Claim(ClaimTypes.Role, role));
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(options.Value.Key));
var credentials = new SigningCredentials(
key, SecurityAlgorithms.HmacSha256);
var expiry = DateTime.UtcNow.AddMinutes(
options.Value.AccessTokenExpiryMinutes);
var token = new JwtSecurityToken(
issuer: options.Value.Issuer,
audience: options.Value.Audience,
claims: claims,
expires: expiry,
signingCredentials: credentials);
return new TokenResponse(
AccessToken: new JwtSecurityTokenHandler().WriteToken(token),
ExpiresAt: expiry,
RefreshToken: GenerateRefreshToken());
}
private static string GenerateRefreshToken()
=> Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
}
public sealed record TokenResponse(
string AccessToken,
DateTime ExpiresAt,
string RefreshToken);
public sealed record RefreshTokenCommand(
string RefreshToken) : IRequest<Result<TokenResponse>>;
internal sealed class RefreshTokenHandler(
ITokenService tokenService,
IRefreshTokenRepository refreshTokenRepo,
IUserRepository userRepo)
: IRequestHandler<RefreshTokenCommand, Result<TokenResponse>>
{
public async Task<Result<TokenResponse>> Handle(
RefreshTokenCommand request, CancellationToken ct)
{
var stored = await refreshTokenRepo
.FindByTokenAsync(request.RefreshToken, ct);
if (stored is null || stored.IsExpired || stored.IsRevoked)
return Result<TokenResponse>.Failure(
Error.Unauthorized("Token.Invalid",
"Invalid or expired refresh token"));
// Rotate: revoke old token
stored.Revoke();
var user = await userRepo.FindAsync(stored.UserId, ct);
var permissions = await userRepo
.GetPermissionsAsync(user!.Id, ct);
var newToken = tokenService.GenerateTokens(
user!, permissions);
// Store new refresh token
await refreshTokenRepo.StoreAsync(
new RefreshToken(user!.Id, newToken.RefreshToken,
DateTime.UtcNow.AddDays(7)),
ct);
await refreshTokenRepo.SaveChangesAsync(ct);
return Result<TokenResponse>.Success(newToken);
}
}
app.MapPost("/auth/login", async (
LoginRequest request,
ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(
new LoginCommand(request.Email, request.Password), ct);
return result.Match(
token => Results.Ok(token),
error => Results.Unauthorized());
});
app.MapPost("/auth/refresh", async (
RefreshTokenRequest request,
ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(
new RefreshTokenCommand(request.RefreshToken), ct);
return result.Match(
token => Results.Ok(token),
error => Results.Unauthorized());
});
public interface ICurrentUserService
{
Guid UserId { get; }
string Email { get; }
IReadOnlyList<string> Roles { get; }
IReadOnlyList<string> Permissions { get; }
}
internal sealed class CurrentUserService(
IHttpContextAccessor httpContextAccessor)
: ICurrentUserService
{
private ClaimsPrincipal User =>
httpContextAccessor.HttpContext?.User
?? throw new UnauthorizedAccessException();
public Guid UserId => Guid.Parse(
User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
public string Email =>
User.FindFirstValue(JwtRegisteredClaimNames.Email)!;
public IReadOnlyList<string> Roles =>
User.FindAll(ClaimTypes.Role)
.Select(c => c.Value).ToList();
public IReadOnlyList<string> Permissions =>
(User.FindFirstValue("permissions") ?? "")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
AddJwtBearer in Program.csJwtSecurityTokenHandler or JsonWebTokenHandler usageTokenValidationParameters configurationITokenService or similar abstraction[Authorize] attributes on controllers/endpointsValidateOnStart for JWT configurationICurrentUserService for claim access in handlersThis skill depends on the always-on security convention rule:
The rule is loaded into context whenever this skill is active (FR-011 universal whitelist).
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.