Linger.AspNetCore.Jwt 1.1.0

dotnet add package Linger.AspNetCore.Jwt --version 1.1.0
                    
NuGet\Install-Package Linger.AspNetCore.Jwt -Version 1.1.0
                    
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.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Linger.AspNetCore.Jwt" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Linger.AspNetCore.Jwt" />
                    
Project file
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.1.0
                    
#r "nuget: Linger.AspNetCore.Jwt, 1.1.0"
                    
#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.1.0
                    
#: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.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Linger.AspNetCore.Jwt&version=1.1.0
                    
Install as a Cake Tool

Linger.AspNetCore.Jwt

Lightweight helpers for issuing and refreshing JWT access tokens in ASP.NET Core, focusing on "simple integration + extensibility + security best practices".

Table of Contents

Features

  • ✅ Interface separation: IJwtService only issues access tokens; refresh logic is decoupled via extension interface
  • ✅ Progressive enhancement: Enable refresh tokens / auto-refresh on demand
  • ✅ Pluggable storage: Memory, database, or custom implementation
  • ✅ Resilience support: Concurrent-safe refresh based on Microsoft.Extensions.Http.Resilience
  • ✅ Security hardening: Supports jti / iat, externalized keys, principle of least privilege
  • ✅ Extensible: Override GetClaimsAsync to add roles / permissions / tenants

Platform

.NET 8.0+ ASP.NET Core

Installation

dotnet add package Linger.AspNetCore.Jwt

Client auto-refresh requires additional packages: Linger.HttpClient.Contracts, Linger.HttpClient.Standard, Microsoft.Extensions.Http.Resilience

Quick Start (Minimal Code)

// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 1. Bind configuration + register authentication scheme
builder.Services.ConfigureJwt(builder.Configuration);
// 2. Basic service
builder.Services.AddScoped<IJwtService, JwtService>();
// 3. Middleware
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// 4. Login endpoint
app.MapPost("/login", async (IJwtService jwt, LoginModel m) =>
{
    if (!UserValidator.Validate(m.Username, m.Password)) return Results.Unauthorized();
    return Results.Ok(await jwt.CreateTokenAsync(m.Username));
});
app.Run();

At this point: You have basic JWT capability; for refresh support → see "Enable Refresh Tokens" below.

JwtOption Configuration

public class JwtOption
{
    public string SecurityKey { get; set; } = null!; // ⚠️ MUST set in production!
    public string Issuer { get; set; } = "Linger.com";
    public string Audience { get; set; } = "Linger.com";
    public int ExpiresInMinutes { get; set; } = 30;               // Access token expiration (minutes)
    public int RefreshTokenExpiresInMinutes { get; set; } = 10080;    // Refresh token expiration (minutes, 7 days)
    public bool EnableRefreshToken { get; set; } = true;  // Whether to enable refresh support
}

appsettings.json:

{
  "JwtOptions": {
    "SecurityKey": "At-least-32-character-production-key (override with SECRET env var)",
    "Issuer": "your-app.com",
    "Audience": "your-api.com",
    "ExpiresInMinutes": 15,
    "RefreshTokenExpiresInMinutes": 10080,
    "EnableRefreshToken": true
  }
}

Environment variable example:

# Linux / macOS
export SECRET="Prod_YourLongSecret_AtLeast32Chars"
# Windows PowerShell
$Env:SECRET = "Prod_YourLongSecret_AtLeast32Chars"

Registration & Integration

// Concise approach
builder.Services.ConfigureJwt(builder.Configuration);

// Manual binding + multi-authentication coexistence
var opt = builder.Configuration.GetGeneric<JwtOption>("JwtOptions");
ArgumentNullException.ThrowIfNull(opt);
builder.Services.AddSingleton(opt);
builder.Services.AddAuthentication(o =>
{
    o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(opt); // Extension

// Optional implementation injection
builder.Services.AddScoped<IJwtService, JwtService>();
builder.Services.AddScoped<IJwtService, CustomJwtService>();
builder.Services.AddScoped<IRefreshableJwtService, MemoryCachedJwtService>();
builder.Services.AddScoped<IJwtService>(sp => sp.GetRequiredService<IRefreshableJwtService>());
builder.Services.AddScoped<IRefreshableJwtService, DbJwtService>();

Custom Claims

Default:

protected virtual Task<List<Claim>> GetClaimsAsync(string userId) =>
    Task.FromResult(new List<Claim>{ new(ClaimTypes.Name, userId) });

Custom:

public class CustomJwtService(AppDbContext db, JwtOption opt, ILogger? logger = null) : 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);
        foreach (var role in user.Roles.Split(','))
            claims.Add(new Claim(ClaimTypes.Role, role));
        return claims;
    }
}

Enable Refresh Tokens

Inherit from the abstract JwtServiceWithRefresh and implement storage:

public class MemoryCachedJwtService : JwtServiceWithRefresh
{
    private readonly IMemoryCache _cache;
    public MemoryCachedJwtService(JwtOption opt, IMemoryCache cache, ILogger<MemoryCachedJwtService>? logger = null) : base(opt, logger) => _cache = cache;
    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? token) && token is not null)
            return Task.FromResult(token);
        throw new Exception("Refresh token not found or expired");
    }
}

Database example (excerpt):

public class DbJwtService : JwtServiceWithRefresh
{
    private readonly IUserRepository _repo;
    public DbJwtService(JwtOption opt, IUserRepository repo, ILogger<DbJwtService>? logger = null) : base(opt, logger) => _repo = repo;
    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);
        if (user is not null && !string.IsNullOrEmpty(user.RefreshToken))
            return new JwtRefreshToken { RefreshToken = user.RefreshToken, ExpiryTime = user.RefreshTokenExpiryTime };
        throw new Exception("Refresh token not found or expired");
    }
}

Controller Example

public class AuthController(IJwtService jwt, IUserService users) : ControllerBase
{
    [HttpPost("login")] 
    public async Task<IActionResult> Login(LoginModel m)
    { 
        var id = await users.ValidateUserAsync(m.Username, m.Password); 
        if (string.IsNullOrEmpty(id)) return Unauthorized(); 
        return Ok(await jwt.CreateTokenAsync(id)); 
    }
    
    [HttpPost("refresh")] 
    public async Task<IActionResult> Refresh(Token token)
    { 
        var result = await jwt.RefreshTokenResultAsync(token);
        if (result.Success) 
            return Ok(result.Token);
        
        return Unauthorized(result.ErrorMessage);
    }
}

💡 Tip: TryRefreshTokenAsync is obsolete, please use RefreshTokenResultAsync instead.

Alternative Approach (using exception handling)

[HttpPost("refresh")] 
public async Task<IActionResult> Refresh(Token token)
{ 
    if (!jwt.SupportsRefreshToken()) 
        return Unauthorized("Refresh token not supported");
    
    try 
    {
        return Ok(await jwt.RefreshTokenAsync(token));
    }
    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, please re-login");
    }
}

💡 Tip: RefreshTokenResultAsync follows the result pattern, avoiding exception overhead for better performance and cleaner code.


Refresh Token Workflow Explained

What is a Refresh Token?

A refresh token is a credential that can be used to obtain new access tokens. When an access token expires, we can use the refresh token to get a new access token from the authentication component.

Feature comparison:

  • Access Token: Short expiration (typically minutes), stored on client
  • Refresh Token: Long expiration (typically days), stored on server database

Token Usage Flow

Refresh Token Flow

  1. Client authenticates by providing credentials (e.g., username and password)
  2. Server issues access token and refresh token after successful validation
  3. Client uses access token to request protected resources
  4. Server validates access token and provides resources
  5. Repeat steps 3-4 until access token expires
  6. After access token expires, client uses refresh token to request new tokens
  7. Server validates refresh token and issues new access token and refresh token
  8. Repeat steps 3-7 until refresh token expires
  9. After refresh token expires, client needs to re-authenticate completely (step 1)

Why Do We Need Refresh Tokens?

So why do we need both access tokens and refresh tokens? Why don't we just set a long expiration date for the access token, like a month or a year? Because if we do that and someone manages to get our access token, they can use it for a long time even if we change our password!

The idea behind refresh tokens is that we can make the access token's lifetime very short, so even if it's compromised, the attacker only has access for a short period. With a refresh token-based flow, the authentication server issues a single-use refresh token along with the access token. The application securely stores the refresh token.

Every time the application sends a request to the server, it sends the access token in the Authorization header, and the server can identify the application using it. Once the access token expires, the server will send a token expired response. After receiving the token expired response, the application sends the expired access token and refresh token to get new access and refresh tokens.

If something goes wrong, the refresh token can be revoked, meaning when the application tries to use it to get a new access token, the request will be denied, and the user must re-enter their credentials and authenticate.

Therefore, refresh tokens help smooth authentication workflows without requiring users to frequently submit their credentials, while not compromising application security.

Security Best Practices

  • Use environment variable SECRET to override config key (length ≥ 32)
  • Short-lived access tokens + longer-lived refresh tokens, revoke promptly
  • Hash persisted refresh tokens (prevent leak abuse)
  • Record jti / iat for revocation and auditing
  • Clear local state immediately on failed refresh

Advanced Features

  • jti / iat claims → Auditing and replay prevention
  • Custom Claims (roles / permissions / tenants / policy tags)
  • Multiple storage backends: Memory / Database / Distributed cache
  • Combined Resilience: Retry + Refresh + Circuit breaker + Timeout

Troubleshooting

Symptom Possible Cause Suggested Fix
401 immediately after successful login Time out of sync / Signature failure Sync time; unify SECRET
Refresh not triggering Not enabled/registered refresh implementation Check EnableRefreshToken & DI
Refresh storm Concurrent 401 race condition Use semaphore/single refresh control
Refresh succeeds but still old token Client not updating headers Confirm event subscription and SetToken call
Invalid signature Inconsistent keys across instances Use config center or unified env variable

FAQ

Q: Must I enable refresh tokens? A: No, you can use short-lived tokens only.

Q: How to revoke all tokens for a user? A: Record jti, add to blacklist; delete refresh token record.

Q: How to support multi-tenancy? A: Add tenant Claim, and validate in authorization policy.

Q: Can I extend the return model? A: Yes, wrap DTO in custom implementation.

Q: How to prevent refresh token theft? A: Store hash on server, client only holds random value, enable HTTPS and minimal persistence.

Product 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.

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