Linger.AspNetCore.Jwt 0.9.8

There is a newer version of this package available.
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
                    
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="0.9.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Linger.AspNetCore.Jwt" Version="0.9.8" />
                    
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 0.9.8
                    
#r "nuget: Linger.AspNetCore.Jwt, 0.9.8"
                    
#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@0.9.8
                    
#: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=0.9.8
                    
Install as a Cake Addin
#tool nuget:?package=Linger.AspNetCore.Jwt&version=0.9.8
                    
Install as a Cake Tool

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 via IRefreshableJwtService
  • Pluggable refresh token persistence (memory / DB / custom)
  • Environment variable SECRET overrides config key
  • Short-lived access tokens, long-lived refresh tokens
  • Adds jti & iat claims 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:

  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 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 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
Loading failed