007
無料Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.
日本語の概要は準備中です。原文の説明を表示しています。
Configures JWT Bearer authentication for .NET APIs. Includes token generation, validation, refresh tokens, and user context extraction from claims.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
This skill implements JWT (JSON Web Token) authentication for .NET APIs:
| 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 |
/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
// 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;
}
{
"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
}
}
}
// 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();
}
// 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);
// 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);
}
}
// 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;
}
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);
}
}
// 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 && !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;
}
}
// 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<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;
}
};
}
}
// 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; }
/// <summary>
/// Current user's email
/// </summary>
string Email { get; }
/// <summary>
/// Current user's organization ID
/// </summary>
Guid? OrganizationId { get; }
/// <summary>
/// Current user's roles
/// </summary>
IReadOnlyList<string> Roles { get; }
/// <summary>
/// Check if user is authenticated
/// </summary>
bool IsAuthenticated { get; }
/// <summary>
/// Check if user has a specific role
/// </summary>
bool IsInRole(string role);
/// <summary>
/// Get a custom claim value
/// </summary>
string? GetClaimValue(string claimType);
}
// 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<string> Roles
{
get
{
return User?.FindAll(ClaimTypes.Role)
.Select(c => c.Value)
.ToList()
?? new List<string>();
}
}
public bool IsInRole(string role)
{
return User?.IsInRole(role) ?? false;
}
public string? GetClaimValue(string claimType)
{
return User?.FindFirst(claimType)?.Value;
}
}
// 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<JwtBearerOptionsSetup>();
// Register services
services.AddHttpContextAccessor();
services.AddScoped<IJwtService, JwtService>();
services.AddScoped<IUserContext, UserContext>();
return services;
}
}
// 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<Result<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<TokenResponse>(UserErrors.InvalidCredentials);
}
// Verify password
if (!_passwordHasher.Verify(request.Password, user.PasswordHash))
{
return Result.Failure<TokenResponse>(UserErrors.InvalidCredentials);
}
// Check if user is active
if (!user.IsActive)
{
return Result.Failure<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);
}
}
// 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<Result<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<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<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;
}
}
// 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<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<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<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);
// 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<IJwtService, JwtService>();
services.AddScoped<IRefreshTokenCookieManager, RefreshTokenCookieManager>();
// Register HttpContextAccessor for UserContext
services.AddHttpContextAccessor();
services.AddScoped<IUserContext, UserContext>();
// Configure JWT Bearer authentication
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
services.ConfigureOptions<JwtBearerOptionsSetup>();
// Configure authorization
services.AddAuthorization();
}
ClockSkew = TimeSpan.Zero// ❌ 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)
permission-authorization - Permission-based access controlapi-controller-generator - Protected API endpointsdotnet-clean-architecture - Infrastructure layer setupまだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.
日本語の概要は準備中です。原文の説明を表示しています。
Guides the creation of agile user stories and Gherkin feature files. Use when the user wants to create a user story, write acceptance criteria, define Gherkin scenarios, or author BDD feature files. This should trigger for requests such as Create a user story; Write a user story; I need to write a user story. Part of cursor-rules-java project
日本語の概要は準備中です。原文の説明を表示しています。
Facilitates conversational discovery to create Architectural Decision Records (ADRs) for non-functional requirements using the ISO/IEC 25010:2023 quality model. Use when the user wants to document quality attributes, NFR decisions, security/performance/scalability architecture, or design systems with measurable quality criteria. This should trigger for requests such as Create ADR for Non-functional requirements; Document Non-functional requirements; Capture Non-functional requirements; Generate Non-functional requirements in an ADR. Part of cursor-rules-java project
日本語の概要は準備中です。原文の説明を表示しています。
Run a health check on an existing project: dependency audit, security scan, test runner detection, CI/CD evaluation, and missing configuration analysis. Maps the three execution gates (pre/in/post) from /10x-bootstrapper to an assessment framework for existing codebases. Reads optional context/foundation/stack-assessment.md from /10x-stack-assess to focus checks on identified gaps. Writes context/foundation/health-check.md with findings, prioritized fixes, and an agent-readiness verdict. Use when the user has an existing project and wants to verify its health before working with an agent. Trigger phrases: "health check", "check my project", "audit my project", "is my project healthy", "sprawdź projekt", "audyt projektu", "health-check", "project health". Use AFTER /10x-stack-assess (brownfield chain), BEFORE agent onboarding (m1-l4).
日本語の概要は準備中です。原文の説明を表示しています。
You MUST use this when building projects end-to-end. Orchestrates all 12 team roles — automatically switches between CTO, architect, PM, engineers, SRE, security, DBA, QA, and EM based on the current phase of work. Starts with brainstorming before any implementation.
日本語の概要は準備中です。原文の説明を表示しています。
Use when you need to add or configure Maven plugins in your pom.xml — including quality tools (enforcer, surefire, failsafe, jacoco, pitest, spotbugs, pmd), security scanning (OWASP), code formatting (Spotless), version management, container image build (Jib), build information tracking, and benchmarking (JMH) — through a consultative, modular step-by-step approach that only adds what you actually need. This should trigger for requests such as Add Maven plugins in pom.xml; Improve Maven plugins in pom.xml. Part of cursor-rules-java project
日本語の概要は準備中です。原文の説明を表示しています。