Linger.AspNetCore.Jwt
1.0.1
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.1
NuGet\Install-Package Linger.AspNetCore.Jwt -Version 1.0.1
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.1" />
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.1" />
<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.1
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Linger.AspNetCore.Jwt, 1.0.1"
#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.1
#: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.1
#tool nuget:?package=Linger.AspNetCore.Jwt&version=1.0.1
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.1)
- Linger.Configuration (>= 1.0.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.0)
-
net8.0
- Linger.AspNetCore.Jwt.Contracts (>= 1.0.1)
- Linger.Configuration (>= 1.0.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.19)
-
net9.0
- Linger.AspNetCore.Jwt.Contracts (>= 1.0.1)
- Linger.Configuration (>= 1.0.0)
- 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