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
<PackageReference Include="Snail.Toolkit.Authentication.JwtBearer" Version="2.0.0" />
<PackageVersion Include="Snail.Toolkit.Authentication.JwtBearer" Version="2.0.0" />
<PackageReference Include="Snail.Toolkit.Authentication.JwtBearer" />
paket add Snail.Toolkit.Authentication.JwtBearer --version 2.0.0
#r "nuget: Snail.Toolkit.Authentication.JwtBearer, 2.0.0"
#:package Snail.Toolkit.Authentication.JwtBearer@2.0.0
#addin nuget:?package=Snail.Toolkit.Authentication.JwtBearer&version=2.0.0
#tool nuget:?package=Snail.Toolkit.Authentication.JwtBearer&version=2.0.0
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
- Always use HTTPS in production
- Rotate
SecretKeyperiodically — a reloadable configuration source is picked up without a restart, and tokens of the previous secret are refused from that moment - Set an appropriate
TokenLifetime - Store
SecretKeysecurely (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager) - Leave
IncludeErrorDetailsoff 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.ValidateandITokenProvider.Refresh, obsolete since 1.1, are gone:ValidateAsynccarries the reason,IRefreshTokens.Mintmints refresh tokens.TokenProviderno longer implementsIDisposable.- The whole
Jwtsection follows a reload, not onlySecretKey: 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.Createsigns with the scheme'sIssuerSigningKey. Aconfiguredelegate that replaces it withIssuerSigningKeysmakesCreatethrow.TokenLifetimeabove a century (52 596 000 minutes) is refused at startup, as zero and negative values were.AddTokenProvider()validates the settings at startup asAddAuthJwtBearerdoes.
1.1
- Every validation flag now defaults to
true. An application that relied on one being off has to say so in configuration. - The
algheader isHS512instead of the XML-dsig URI, and the algorithm is pinned on validation: tokens issued by earlier versions are refused. SecretKeyshorter than 64 characters is refused at startup; HMAC-SHA512 never accepted it.ITokenProvider.Validateis obsolete in favour ofValidateAsync, which carries the reason.- Refresh tokens are url-safe base64.
ITokenProvider.Refreshis obsolete in favour ofIRefreshTokens.Mint.TokenValidationis(TokenRefusal Refusal, string? Fault);IsValidis derived from it.
License
Snail.Toolkit.Authentication.JwtBearer is a free and open source project, released under the permissible MIT license.
| Product | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.12)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.