Linger.AspNetCore.Jwt
0.9.8
See the version list below for details.
dotnet add package Linger.AspNetCore.Jwt --version 0.9.8
NuGet\Install-Package Linger.AspNetCore.Jwt -Version 0.9.8
<PackageReference Include="Linger.AspNetCore.Jwt" Version="0.9.8" />
<PackageVersion Include="Linger.AspNetCore.Jwt" Version="0.9.8" />
<PackageReference Include="Linger.AspNetCore.Jwt" />
paket add Linger.AspNetCore.Jwt --version 0.9.8
#r "nuget: Linger.AspNetCore.Jwt, 0.9.8"
#:package Linger.AspNetCore.Jwt@0.9.8
#addin nuget:?package=Linger.AspNetCore.Jwt&version=0.9.8
#tool nuget:?package=Linger.AspNetCore.Jwt&version=0.9.8
Linger.AspNetCore.Jwt
View this document in: English | 中文
Full legacy guide: legacy-full-guide.md
Lightweight helpers for issuing and refreshing JWT access tokens in ASP.NET Core.
Features
- Clean core
IJwtService+ opt-in refresh viaIRefreshableJwtService - Pluggable refresh token persistence (memory / DB / custom)
- Environment variable
SECREToverrides config key - Short-lived access tokens, long-lived refresh tokens
- Adds
jti&iatclaims for replay protection & auditing
Platform
.NET 8+.
Install from NuGet (package name placeholder):
dotnet add package Linger.AspNetCore.Jwt
Installation
In Program.cs:
// Read JwtOptions & configure authentication
var jwtOptions = builder.Configuration.GetSection("JwtOptions").Get<JwtOption>()!;
builder.Services.AddSingleton(jwtOptions);
builder.Services.AddAuthentication().AddJwtBearer(jwtOptions);
// Choose ONE registration style
// 1. Basic (no refresh)
builder.Services.AddScoped<IJwtService, JwtService>();
// 2. Custom claims (no refresh)
builder.Services.AddScoped<IJwtService, CustomJwtService>();
// 3. Refresh (memory cache)
builder.Services.AddMemoryCache();
builder.Services.AddScoped<IRefreshableJwtService, MemoryCachedJwtService>();
builder.Services.AddScoped<IJwtService>(sp => sp.GetRequiredService<IRefreshableJwtService>());
appsettings.json (minimal):
{
"JwtOptions": {
"SecurityKey": "your-32+chars-secret",
"Issuer": "your-app",
"Audience": "your-api",
"Expires": 15,
"RefreshTokenExpires": 10080,
"EnableRefreshToken": true
}
}
Environment precedence: SECRET env var > JwtOptions:SecurityKey.
JwtOption
public sealed class JwtOption
{
public string SecurityKey { get; set; } = "dev-secret-change";
public string Issuer { get; set; } = "example";
public string Audience { get; set; } = "example";
public int Expires { get; set; } = 15; // minutes
public int RefreshTokenExpires { get; set; } = 10080; // minutes (7d)
public bool EnableRefreshToken { get; set; } = true;
}
Custom Claims
Extend JwtService or override GetClaimsAsync:
sealed class CustomJwtService(AppDbContext db, JwtOption opt, ILogger<CustomJwtService> logger)
: JwtService(opt, logger)
{
protected override async Task<List<Claim>> GetClaimsAsync(string userId)
{
var claims = new List<Claim> { new(ClaimTypes.Name, userId) };
var user = await db.Users.FindAsync(userId);
if (user is not null)
{
foreach (var role in user.Roles.Split(','))
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
}
return claims;
}
}
Refresh Token Implementations
Memory cache example:
sealed class MemoryCachedJwtService(JwtOption opt, IMemoryCache cache, ILogger<MemoryCachedJwtService> logger)
: JwtServiceWithRefresh(opt, logger)
{
protected override Task HandleRefreshToken(string userId, JwtRefreshToken token)
{
cache.Set($"RT_{userId}", token, TimeSpan.FromMinutes(_jwtOptions.RefreshTokenExpires));
return Task.CompletedTask;
}
protected override Task<JwtRefreshToken> GetExistRefreshTokenAsync(string userId)
{
if (cache.TryGetValue($"RT_{userId}", out JwtRefreshToken? t) && t is not null)
{
return Task.FromResult(t);
}
throw new InvalidOperationException("Refresh token missing or expired");
}
}
Database example sketch (pseudo):
sealed class DbJwtService(JwtOption opt, IUserRepository repo, ILogger<DbJwtService> logger)
: JwtServiceWithRefresh(opt, logger)
{
protected override Task HandleRefreshToken(string userId, JwtRefreshToken token)
=> repo.UpdateRefreshTokenAsync(userId, token.RefreshToken, token.ExpiryTime);
protected override async Task<JwtRefreshToken> GetExistRefreshTokenAsync(string userId)
{
var user = await repo.GetUserAsync(userId) ?? throw new InvalidOperationException("User not found");
if (string.IsNullOrWhiteSpace(user.RefreshToken)) throw new InvalidOperationException("No stored refresh token");
return new JwtRefreshToken { RefreshToken = user.RefreshToken, ExpiryTime = user.RefreshTokenExpiryTime };
}
}
Controller Example
[ApiController]
[Route("api/auth")]
sealed class AuthController(IJwtService jwt) : ControllerBase
{
[HttpPost("login")]
public async Task<IActionResult> Login(LoginDto dto)
{
// validate credentials -> userId
var userId = dto.UserName; // placeholder
var token = await jwt.CreateTokenAsync(userId);
return Ok(token);
}
[HttpPost("refresh")]
public async Task<IActionResult> Refresh(TokenEnvelope dto)
{
if (jwt.SupportsRefreshToken())
{
var (ok, newToken) = await jwt.TryRefreshTokenAsync(dto);
if (ok) return Ok(newToken);
}
return Unauthorized();
}
}
Client Refresh (Concept)
Use retry/resilience pipeline to intercept 401, refresh once (semaphore guarded), retry original request. See legacy guide for full client examples (Blazor, WinForms, simple HttpClient wrapper).
Workflow:
- Login → access + refresh
- Use access until 401
- Refresh endpoint with pair
- Issue new pair (rotate refresh)
- Deny if refresh invalid/expired → re-login
Security
- Store production secret in environment:
SECRET - Rotate secrets & refresh tokens on each refresh
- Include
jti(unique id) &iat(issued at) - Keep access tokens short-lived (minutes)
- Persist refresh tokens server-side; treat as confidential credentials
Advanced
- Add custom claim sources (roles, permissions)
- Central revoke list keyed by
jti - Multi-tenant: include
tenant_idclaim - Pairwise rotation: invalidate previous refresh token on use
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 401 immediately | Clock skew | Sync server time / allow small skew |
| Refresh succeeds but old token still works | Not rotating | Ensure refresh rotates and revoke old jti |
| Memory implementation loses tokens | App recycled | Use persistent store |
| Env SECRET ignored | Name mismatch | Ensure variable EXACT name SECRET |
FAQ
Q: Why extension + interface for refresh?
A: Keeps core minimal; refresh optional.
Q: Can I disable refresh?
A: Set EnableRefreshToken = false and only register IJwtService.
Q: How to revoke a single token?
A: Track jti in a deny list until natural expiry.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Linger.AspNetCore.Jwt.Contracts (>= 0.9.8)
- Linger.Configuration (>= 0.9.8)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.0-rc.1.25451.107)
-
net8.0
- Linger.AspNetCore.Jwt.Contracts (>= 0.9.8)
- Linger.Configuration (>= 0.9.8)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.19)
-
net9.0
- Linger.AspNetCore.Jwt.Contracts (>= 0.9.8)
- Linger.Configuration (>= 0.9.8)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 9.0.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0 | 98 | 2/4/2026 |
| 1.0.3-preview | 97 | 1/9/2026 |
| 1.0.2-preview | 100 | 1/8/2026 |
| 1.0.1 | 210 | 11/24/2025 |
| 1.0.1-preview | 284 | 11/13/2025 |
| 1.0.0 | 306 | 11/12/2025 |
| 1.0.0-preview2 | 169 | 11/6/2025 |
| 1.0.0-preview1 | 159 | 11/5/2025 |
| 0.9.9 | 158 | 10/16/2025 |
| 0.9.8 | 162 | 10/14/2025 |
| 0.9.7-preview | 158 | 10/13/2025 |
| 0.9.6-preview | 130 | 10/12/2025 |
| 0.9.5 | 130 | 9/28/2025 |
| 0.9.4-preview | 162 | 9/25/2025 |
| 0.9.3-preview | 186 | 9/22/2025 |
| 0.9.1-preview | 290 | 9/16/2025 |
| 0.9.0-preview | 109 | 9/12/2025 |
| 0.8.5-preview | 222 | 8/31/2025 |
| 0.8.4-preview | 336 | 8/25/2025 |
| 0.8.3-preview | 219 | 8/20/2025 |