Linger.AspNetCore.Jwt
1.0.2-preview
This is a prerelease version of Linger.AspNetCore.Jwt.
There is a newer version of this package available.
See the version list below for details.
See the version list below for details.
dotnet add package Linger.AspNetCore.Jwt --version 1.0.2-preview
NuGet\Install-Package Linger.AspNetCore.Jwt -Version 1.0.2-preview
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Linger.AspNetCore.Jwt" Version="1.0.2-preview" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Linger.AspNetCore.Jwt" Version="1.0.2-preview" />
<PackageReference Include="Linger.AspNetCore.Jwt" />
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Linger.AspNetCore.Jwt --version 1.0.2-preview
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Linger.AspNetCore.Jwt, 1.0.2-preview"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Linger.AspNetCore.Jwt@1.0.2-preview
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Linger.AspNetCore.Jwt&version=1.0.2-preview&prerelease
#tool nuget:?package=Linger.AspNetCore.Jwt&version=1.0.2-preview&prerelease
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
Linger.AspNetCore.Jwt
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",
"ExpiresInMinutes": 15,
"RefreshTokenExpiresInMinutes": 10080,
"EnableRefreshToken": true
}
}
Environment precedence: SECRET env var > JwtOptions:SecurityKey.
JwtOption
public sealed class JwtOption
{
public string SecurityKey { get; set; } = null!; // MUST set in production!
public string Issuer { get; set; } = "example";
public string Audience { get; set; } = "example";
public int ExpiresInMinutes { get; set; } = 30; // Token expiration (minutes)
public int RefreshTokenExpiresInMinutes { get; set; } = 10080; // Refresh token expiration (minutes, 7 days)
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.RefreshTokenExpiresInMinutes));
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
## Controller Example
### Recommended Approach (using TryRefreshTokenAsync)
```csharp
public 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)
{
var result = await jwt.TryRefreshTokenAsync(dto);
if (result.Success)
return Ok(result.Token);
return Unauthorized(result.ErrorMessage);
}
}
Alternative Approach (using exception handling)
[HttpPost("refresh")]
public async Task<IActionResult> Refresh(TokenEnvelope dto)
{
if (!jwt.SupportsRefreshToken())
return Unauthorized("Refresh token not supported");
try
{
return Ok(await jwt.RefreshTokenAsync(dto));
}
catch (NotSupportedException)
{
return Unauthorized("Refresh token not supported");
}
catch (SecurityTokenException ex)
{
_logger.LogWarning(ex, "Refresh token validation failed");
return Unauthorized("Invalid or expired refresh token");
}
}
💡 Tip: The
TryRefreshTokenAsyncmethod follows C#'s Try-pattern convention, offering better performance and cleaner code.
## 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:
1. Login -> access + refresh
2. Use access until 401
3. Refresh endpoint with pair
4. Issue new pair (rotate refresh)
5. 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_id` claim
- 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. |
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
-
net10.0
- Linger.AspNetCore.Jwt.Contracts (>= 1.0.2-preview)
- Linger.Configuration (>= 1.0.2-preview)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.1)
-
net8.0
- Linger.AspNetCore.Jwt.Contracts (>= 1.0.2-preview)
- Linger.Configuration (>= 1.0.2-preview)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.19)
-
net9.0
- Linger.AspNetCore.Jwt.Contracts (>= 1.0.2-preview)
- Linger.Configuration (>= 1.0.2-preview)
- 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 | 209 | 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 | 218 | 8/20/2025 |
Loading failed