JWT authentication and authorization library for .NET APIs. Handles token issuing, refresh token rotation with reuse detection, role and claim-based access control, token blacklisting, key rotation, and ASP.NET Core middleware integration -- with secure defaults and minimal configuration.
- JWT Issuing -- Generate signed access tokens with configurable expiry, audience, issuer, and custom claims. Supports HS256/384/512, RS256/384/512, and ES256/384/512 algorithms.
- Refresh Token Rotation -- Single-use refresh tokens with automatic rotation. Reuse of a consumed token triggers family-wide revocation to mitigate token theft.
- Role-Based Access Control -- Define roles in token requests and enforce them via
[Authorize(Roles = "admin")]or customRoleRequirementpolicies. - Claim-Based Access Control -- Fine-grained authorization using custom claims with
ClaimRequirementand built-in authorization handlers. - Token Blacklisting -- Revoke individual access tokens before expiry. In-memory store included; bring your own Redis or database implementation.
- Key Rotation -- Support multiple signing keys with time-based activation windows. Old keys remain valid for verification while new keys are used for signing.
- Claim Transformation -- Plug in
IClaimTransformerimplementations to enrich claims during token issuance (e.g., load roles from a database). - Middleware Integration -- Authentication handler that plugs into ASP.NET Core's
UseAuthentication()/UseAuthorization()pipeline. Single registration withservices.AddAuthKit(). - Background Cleanup -- Automatic expiry of blacklisted and consumed refresh tokens via a configurable background service.
- Extensible Storage -- Default in-memory stores for both token blacklist and refresh tokens. Replace with your own
ITokenBlacklistStoreandIRefreshTokenStoreimplementations for distributed deployments.
dotnet add package JG.AuthKitRequires .NET 8.0 or later.
builder.Services.AddAuthKit(options =>
{
options.Secret = builder.Configuration["Jwt:Secret"];
options.Issuer = "my-api";
options.Audience = "my-app";
options.AccessTokenExpiry = TimeSpan.FromMinutes(15);
options.RefreshTokenExpiry = TimeSpan.FromDays(30);
});var app = builder.Build();
app.UseAuthKit();
app.MapControllers();
app.Run();app.MapPost("/auth/login", async (LoginRequest login, ITokenService tokenService) =>
{
// Validate credentials against your user store
var user = await userService.ValidateAsync(login.Email, login.Password);
if (user is null)
return Results.Unauthorized();
var result = await tokenService.IssueTokenAsync(new TokenRequest
{
Subject = user.Id,
Roles = user.Roles.ToList(),
Claims = [new Claim("tenant", user.TenantId)],
});
return Results.Ok(new
{
result.AccessToken,
result.RefreshToken,
result.ExpiresAt,
});
});app.MapPost("/auth/refresh", async (RefreshBody body, IRefreshTokenService refreshService) =>
{
try
{
var result = await refreshService.RefreshAsync(new RefreshRequest
{
RefreshToken = body.RefreshToken,
});
return Results.Ok(new { result.AccessToken, result.RefreshToken, result.ExpiresAt });
}
catch (InvalidOperationException ex)
{
return Results.Problem(ex.Message, statusCode: 401);
}
});// Require authentication
app.MapGet("/me", [Authorize] (ClaimsPrincipal user) =>
{
var sub = user.FindFirst("sub")?.Value;
return Results.Ok(new { UserId = sub });
});
// Require specific role
app.MapGet("/admin", [Authorize(Roles = "admin")] () => Results.Ok("Admin area"));app.MapPost("/auth/logout", async (LogoutBody body, ITokenService tokenService,
IRefreshTokenService refreshService) =>
{
// Blacklist the access token
await tokenService.RevokeTokenAsync(body.AccessToken);
// Revoke the refresh token family
if (body.RefreshToken is not null)
await refreshService.RevokeAsync(body.RefreshToken);
return Results.Ok();
});Support multiple signing keys with time-based activation for zero-downtime key rotation:
builder.Services.AddAuthKit(options =>
{
options.Issuer = "my-api";
// Old key: still valid for verification, no longer signs new tokens
options.SigningKeys.Add(new SigningKeyDescriptor
{
KeyId = "key-2025-06",
Key = new SymmetricSecurityKey(Convert.FromBase64String(oldKey)),
Algorithm = "HS256",
ActiveFrom = new DateTimeOffset(2025, 6, 1, 0, 0, 0, TimeSpan.Zero),
ActiveUntil = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
});
// Current key: used for signing
options.SigningKeys.Add(new SigningKeyDescriptor
{
KeyId = "key-2026-01",
Key = new SymmetricSecurityKey(Convert.FromBase64String(currentKey)),
Algorithm = "HS256",
ActiveFrom = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
});
});The default in-memory stores work for single-instance deployments. For distributed setups, register your own implementations before calling AddAuthKit:
// Register before AddAuthKit -- these won't be overwritten
builder.Services.AddSingleton<ITokenBlacklistStore, RedisTokenBlacklistStore>();
builder.Services.AddSingleton<IRefreshTokenStore, PostgresRefreshTokenStore>();
builder.Services.AddAuthKit(options => { ... });Enrich claims at token issuance time:
public class RoleEnricher : IClaimTransformer
{
private readonly IRoleRepository _roles;
public RoleEnricher(IRoleRepository roles) => _roles = roles;
public async ValueTask<IReadOnlyList<Claim>> TransformAsync(
IReadOnlyList<Claim> claims, CancellationToken cancellationToken)
{
var sub = claims.FirstOrDefault(c => c.Type == "sub")?.Value;
if (sub is null) return claims;
var roles = await _roles.GetRolesAsync(sub, cancellationToken);
var enriched = new List<Claim>(claims);
foreach (var role in roles)
enriched.Add(new Claim(ClaimTypes.Role, role));
return enriched;
}
}
// Register:
builder.Services.AddSingleton<IClaimTransformer, RoleEnricher>();builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOrEditor", policy =>
policy.AddRequirements(new RoleRequirement("admin", "editor")));
options.AddPolicy("HasTenant", policy =>
policy.AddRequirements(new ClaimRequirement("tenant")));
options.AddPolicy("Engineering", policy =>
policy.AddRequirements(new ClaimRequirement("department", "engineering", "devops")));
});| Option | Default | Description |
|---|---|---|
Secret |
-- | HMAC signing key (min 32 bytes UTF-8) |
SigningKey |
-- | SecurityKey for RSA/ECDSA signing |
SigningAlgorithm |
"HS256" |
Signing algorithm |
Issuer |
-- | Token issuer (iss claim) |
Audience |
-- | Token audience (aud claim) |
AccessTokenExpiry |
15 min | Access token lifetime |
RefreshTokenExpiry |
30 days | Refresh token lifetime |
ClockSkew |
1 min | Validation clock skew tolerance |
EnableRefreshTokenRotation |
true |
Enable refresh token rotation |
EnableTokenBlacklist |
true |
Enable token revocation |
CleanupInterval |
5 min | Background cleanup interval |
- API Reference -- Detailed API docs with examples for every feature
Contributions are welcome. Please feel free to submit a Pull Request.
Run performance benchmarks with BenchmarkDotNet:
# List all available benchmarks
dotnet run --project tests/dotnet-auth-kit.Benchmarks -c Release -- --list flat
# Run all benchmarks
dotnet run --project tests/dotnet-auth-kit.Benchmarks -c Release -- --filter *
# Run token service benchmarks only
dotnet run --project tests/dotnet-auth-kit.Benchmarks -c Release -- --filter *TokenService*Licensed under the Apache License 2.0. See LICENSE for details.