Snail.Toolkit.Authentication.JwtBearer 2.0.0

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

Snail.Toolkit.Authentication.JwtBearer

JWT Bearer authentication for ASP.NET Core in one call. The Jwt section is bound and validated when the host starts, the Bearer scheme is configured from it, and ITokenProvider issues and checks tokens signed with HMAC-SHA512 by the very same rules.

Every check is on by default, the caller's own options win over the package's, a changed section is picked up without a restart, and a second issuer registers under its own key. .NET 10, Microsoft dependencies only.

Installation

dotnet add package Snail.Toolkit.Authentication.JwtBearer

Configuration

Service Registration

// Program.cs
builder.Services.AddAuthJwtBearer(builder.Configuration);

Middleware Setup

// Program.cs
app.UseAuthentication();  // Must come before UseAuthorization
app.UseAuthorization();   // Required for [Authorize] attributes

appsettings.json Configuration

{
  "Jwt": {
    "Issuer": "your_issuer",
    "Audience": "your_audience",
    "SecretKey": "minimum_64_character_secure_key_here_because_hmac_sha512_says_so",
    "TokenLifetime": 60
  }
}
Configuration Options
Setting Required Default Description
Issuer Yes - Token publisher identifier
Audience Yes - Intended token recipient
SecretKey Yes - Minimum 64-character secure key
ValidateAudience No true Validate token audience
ValidateIssuer No true Validate token issuer
ValidateLifetime No true Validate token expiration
ValidateIssuerSigningKey No true Validate the signing key
TokenLifetime No 60 Token validity in minutes
ClockSkew No 00:05:00 Tolerated clock drift between issuer and validator
RequireHttpsMetadata No true Require HTTPS for the authority
IncludeErrorDetails No false Tell a rejected caller why it was rejected

Every check defaults to on. Turning one off is an explicit decision, written in configuration.

The whole section follows a reload of the configuration: the scheme's options are built again from it, the middleware and ITokenProvider judge by the new rules on the next token, and a token is minted for the rules, with the key, it will be judged by. A section that reloads into something invalid stops both minting and judging with the fault naming the key, rather than keeping the last good secret.

The section name is a parameter, so the settings can live wherever the application keeps them:

builder.Services.AddAuthJwtBearer(builder.Configuration, sectionName: "Partners");

A repeated call from the same section is a no-op; from another section it fails and says what to do instead, rather than quietly binding over the first registration.

A second issuer

builder.Services.AddAuthJwtBearer(builder.Configuration);              // section Jwt,      scheme Bearer
builder.Services.AddKeyedAuthJwtBearer(builder.Configuration, "Partners"); // section Partners, scheme Partners

Settings, scheme and provider are all named after the key, so the two share nothing:

public class PartnerController([FromKeyedServices("Partners")] ITokenProvider tokens) : ControllerBase
{
    [HttpGet("me"), Authorize(AuthenticationSchemes = "Partners")]
    public IActionResult Me() => Ok();
}

The keyed call sets no default scheme on purpose — an endpoint says which issuer it trusts.

The settings are validated when the host starts. A missing or short secret refuses to bring the process up, naming the key it choked on, instead of failing on the first login.

Advanced Usage

Custom JWT Bearer Options

The delegate runs after this package has applied its own settings, so anything it sets wins. It runs again each time the section reloads.

builder.Services.AddAuthJwtBearer(builder.Configuration, options =>
{
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context => Task.CompletedTask
    };
});

ITokenProvider signs with TokenValidationParameters.IssuerSigningKey, the key the scheme is judged by. A delegate that replaces it with IssuerSigningKeys has taken the keys into its own hands, and Create refuses to guess which one to sign with.

Issuing tokens without the request pipeline

A service that only mints tokens does not need the authentication middleware:

builder.Services.AddTokenProvider();

Security Considerations

  1. Always use HTTPS in production
  2. Rotate SecretKey periodically — a reloadable configuration source is picked up without a restart, and tokens of the previous secret are refused from that moment
  3. Set an appropriate TokenLifetime
  4. Store SecretKey securely (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager)
  5. Leave IncludeErrorDetails off outside development: it tells a caller whether a token expired or was never signed by this application

Samples

1. Token Creation Example

var claims = new List<Claim>
{
    new(JwtRegisteredClaimNames.Sub, "user123"),
    new(JwtRegisteredClaimNames.Name, "John Doe"),
    new(JwtRegisteredClaimNames.Email, "john@example.com"),
    new("role", "Admin")
};

var token = tokenProvider.Create(claims);

Claims are written exactly as given. Prefer the short registered names above over ClaimTypes.*, whose values are XML schema URIs that inflate every request and mean nothing outside .NET.

Tokens are signed with HMAC-SHA512 and carry "alg": "HS512".

2. Refresh Token Generation Example

Refresh tokens live behind their own contract: nothing about them is signed, parsed or configured.

var refreshToken = refreshTokens.Mint();       // 32 bytes
var longRefreshToken = refreshTokens.Mint(64); // 64 bytes

Url-safe base64, so it survives a query string or a cookie unescaped. A size below 16 bytes is refused rather than silently producing a weak token.

3. Token Validation Example

var verdict = await tokenProvider.ValidateAsync(token, cancellationToken);

var answer = verdict.Refusal switch
{
    TokenRefusal.None => Results.Ok(),
    TokenRefusal.Expired => Results.Unauthorized(),   // the client should refresh
    _ => Results.Forbid()                             // the client should log in again
};

TokenValidation carries the reason as TokenRefusal — Expired, NotYetValid, Signature, Audience, Issuer, Malformed, Other — with the library's own message in Fault. IsValid is derived from the refusal, so the two cannot disagree.

4. Integration with Controllers

[ApiController]
[Route("auth")]
public class AuthController(ITokenProvider tokenProvider) : ControllerBase
{
    [HttpPost("login")]
    public IActionResult Login([FromBody] LoginRequest request, [FromServices] IRefreshTokens refreshTokens)
    {
        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, request.Username),
            new("custom_claim", "value")
        };

        return Ok(new
        {
            Token = tokenProvider.Create(claims),
            RefreshToken = refreshTokens.Mint()
        });
    }
}

Observability

The package publishes two counters under the meter Snail.Toolkit.Authentication.JwtBearer:

Instrument Tags What it says
auth.tokens.issued auth.scheme Access tokens this application signed
auth.tokens.refused auth.scheme, auth.refusal Tokens it turned away, by reason

The meter is taken from the host's IMeterFactory when there is one, so AddMetrics and an OTLP exporter pick it up with no further wiring. Each refusal is also logged at Debug when a logger is registered: the counter answers whether tokens are expiring or arriving unsigned, the log answers which token it was.

Upgrading

2.0

  • ITokenProvider.Validate and ITokenProvider.Refresh, obsolete since 1.1, are gone: ValidateAsync carries the reason, IRefreshTokens.Mint mints refresh tokens. TokenProvider no longer implements IDisposable.
  • The whole Jwt section follows a reload, not only SecretKey: a changed issuer, audience, flag or skew applies to the next token. A section that reloads into something invalid stops minting and judging with the options fault instead of keeping the last good secret.
  • ITokenProvider.Create signs with the scheme's IssuerSigningKey. A configure delegate that replaces it with IssuerSigningKeys makes Create throw.
  • TokenLifetime above a century (52 596 000 minutes) is refused at startup, as zero and negative values were.
  • AddTokenProvider() validates the settings at startup as AddAuthJwtBearer does.

1.1

  • Every validation flag now defaults to true. An application that relied on one being off has to say so in configuration.
  • The alg header is HS512 instead of the XML-dsig URI, and the algorithm is pinned on validation: tokens issued by earlier versions are refused.
  • SecretKey shorter than 64 characters is refused at startup; HMAC-SHA512 never accepted it.
  • ITokenProvider.Validate is obsolete in favour of ValidateAsync, which carries the reason.
  • Refresh tokens are url-safe base64.
  • ITokenProvider.Refresh is obsolete in favour of IRefreshTokens.Mint.
  • TokenValidation is (TokenRefusal Refusal, string? Fault); IsValid is derived from it.

License

Snail.Toolkit.Authentication.JwtBearer is a free and open source project, released under the permissible MIT license.

Product Compatible and additional computed target framework versions.
.NET 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
2.0.0 101 10/21/2026
1.1.0 213 9/6/2026
1.0.0 2,008 5/30/2025