PostQuantum.AspNetCore
1.0.0-preview.3
dotnet add package PostQuantum.AspNetCore --version 1.0.0-preview.3
NuGet\Install-Package PostQuantum.AspNetCore -Version 1.0.0-preview.3
<PackageReference Include="PostQuantum.AspNetCore" Version="1.0.0-preview.3" />
<PackageVersion Include="PostQuantum.AspNetCore" Version="1.0.0-preview.3" />
<PackageReference Include="PostQuantum.AspNetCore" />
paket add PostQuantum.AspNetCore --version 1.0.0-preview.3
#r "nuget: PostQuantum.AspNetCore, 1.0.0-preview.3"
#:package PostQuantum.AspNetCore@1.0.0-preview.3
#addin nuget:?package=PostQuantum.AspNetCore&version=1.0.0-preview.3&prerelease
#tool nuget:?package=PostQuantum.AspNetCore&version=1.0.0-preview.3&prerelease
PostQuantum.AspNetCore
The high-level ASP.NET Core integration for post-quantum JWT
authentication. Add one line — AddPostQuantumJwtBearer(…) — and
hybrid ML-DSA-65 + X-Wing tokens authenticate through the standard
AuthenticationBuilder exactly the way AddJwtBearer always has.
[Authorize] attributes, policies, role checks, claims, middleware —
everything downstream of the wireup works unchanged, because the
handler emits a real ClaimsPrincipal.
Built on PostQuantum.Jwt
and the native .NET 10 BCL post-quantum primitives. Fail-closed by
construction. Small surface. Honest about its limits.
What this package is: a thin, opinionated application layer for ASP.NET Core authentication. Extension methods, an
AuthenticationHandler, event hooks, a JWKS-equivalent key ring, a hosted-service warmup, and metrics + tracing — all the wiring you'd otherwise write yourself to make post-quantum JWTs feel native toAddAuthentication.What this package is not: a cryptography library. No implementation of ML-DSA, ML-KEM, X25519, AES-GCM, or SHA-3 lives in here. We don't compete with BouncyCastle, liboqs, or
System.Security.Cryptography. The actual signing, verification, key encapsulation, and content encryption all happen insidePostQuantum.Jwt, which in turn uses the FIPS-validated .NET 10 BCL post-quantum primitives (with BouncyCastle for the one piece the BCL doesn't ship: X25519). Think ofPostQuantum.AspNetCoreas theAddJwtBearerequivalent that knows the right things about ML-DSA-65 — not a reinvention of the crypto stack underneath.
Status —
1.0.0-preview.3. Preview software. Not for production use. The API may change before 1.0, and the underlying cryptographic construction has not been independently audited. ReadKNOWN-GAPS.mdbefore depending on this for anything that matters.
Most ASP.NET Core apps should use this package. It is the high-level, one-line wire-up for post-quantum JWT bearer auth — event hooks, hosted- service warmup,
Meter+ActivitySourceobservability, distributed replay-cache wiring, and a DI helper that doesn't force aBuildServiceProvider()call. The engine repository also ships a lower- levelPostQuantum.Jwt.AspNetCorepackage; that one is the minimal "prove the engine works in ASP.NET Core" surface and lacks those additions. Pick it only if you are deliberately building your own application-layer wiring on top of the engine.
Operator note: the HTTP key directory is the root of trust for token validation. Read
SECURITY.md#trust-root-the-http-key-directorybefore shipping — operators are expected to configure certificate pinning or a hardenedHttpClienton the directory endpoint. The library has no insecure fallback by design; if the fetch fails closed, validation fails closed.
Highlights
- One-line wireup —
AddPostQuantumJwtBearer(…)slots into the standardAuthenticationBuilderexactly likeAddJwtBearer. - Fail-closed by construction — every validation failure becomes
401. Noalg: none, no algorithm fallback, no degraded path. - Distributed replay protection — single-use
jtienforcement across your fleet via thePostQuantum.AspNetCore.RedisReplayCachecompanion package (SET NX + remaining-token-TTL). - JWKS-equivalent key rotation —
IPostQuantumJwtKeyRingwith an HTTP-backed implementation, atomic snapshot swap on refresh, unknown-kidthrottling, hosted-service startup warmup. - Four event hooks —
OnMessageReceived(SignalR-style alternate token transports),OnTokenValidated(enrich principal),OnAuthenticationFailed,OnChallenge. - First-class observability —
System.Diagnostics.Metrics+ anActivitySourcefor OpenTelemetry / Prometheus / Application Insights. - AOT-compatible —
IsAotCompatible=true, verified end-to-end in CI on Linux, Windows, and macOS. - Honest about limits — preview status, non-IANA algorithm
identifiers, no independent audit, every gap tracked in
KNOWN-GAPS.md.
In a hurry? Jump straight to:
- Getting started — zero to working PQ API in 10 minutes.
- Migrating from
AddJwtBearer— side-by-side diff.- Security model — what the library protects, what it doesn't, replay-protection requirements.
- Recipes — copy-paste-able scenarios: Redis replay, OpenTelemetry, SignalR, multi-tenant, multi-scheme, Swagger, Docker/K8s.
- FAQ — should I use this in production? how big are tokens? does it work with Auth0? — and 15 more.
- Production checklist — before user traffic hits.
Where does this fit in the stack?
┌──────────────────────────────────────────────────────────────────────┐
│ Your ASP.NET Core app │
│ builder.Services.AddAuthentication().AddPostQuantumJwtBearer(...) │
├──────────────────────────────────────────────────────────────────────┤
│ PostQuantum.AspNetCore (this lib) │
│ · AuthenticationHandler + options + 4 event hooks │
│ · IPostQuantumJwtKeyRing (JWKS-equivalent) │
│ · Hosted-service warmup, metrics, tracing │
├──────────────────────────────────────────────────────────────────────┤
│ PostQuantum.Jwt (the engine, separate pkg) │
│ · PqJwtBuilder / PqJwtValidator │
│ · X-Wing combiner, JWE wire format, replay cache │
├──────────────────────────────────────────────────────────────────────┤
│ Crypto primitives (not this lib) │
│ · System.Security.Cryptography.MLDsa / MLKem (.NET 10 BCL) │
│ · BouncyCastle.Cryptography (X25519 only) │
└──────────────────────────────────────────────────────────────────────┘
This library sits at the top of that stack — the application integration layer. It does no cryptography of its own. If you're looking for raw ML-DSA, ML-KEM, X25519, or AES-GCM, those live in the .NET BCL and BouncyCastle and we are happy customers, not competitors.
Table of contents
- Where does this fit in the stack?
- Why
- Install
- 60-second tour
- Usage
- Sample apps
- Public API at a glance
- Defaults and what they mean
- Compared to
Microsoft.AspNetCore.Authentication.JwtBearer - Migrating from
AddJwtBearer - Migrating from
PostQuantum.Jwt.AspNetCore - Security posture
- Compatibility
- Building from source
- Contributing
- License
Why
Why a separate package, when you could just call PostQuantum.Jwt
yourself from your ASP.NET Core app? Because authentication wiring is
where the bugs live. Token retrieval from Authorization (or ?access_token=
for SignalR), case-insensitive Bearer prefix matching, the
WWW-Authenticate challenge response with RFC-compliant realm escaping,
event hooks for principal enrichment, key-ring rotation, hosted-service
cache warmup, fail-closed handling of every exception path, metrics for
ops dashboards, distributed-tracing spans — Microsoft.AspNetCore.Authentication.JwtBearer
does all of that for the classical algorithms. This library does it
for ML-DSA-65. You shouldn't have to write the wiring yourself.
Why post-quantum at all? A cryptographically relevant quantum computer would break the elliptic-curve math behind every JWT signature in production today (EdDSA, ECDSA, RSA). Pure post-quantum schemes are new and comparatively under-attacked. Hybrid hedges both at once:
- Signatures — ML-DSA-65 (FIPS 204). NIST-standardised lattice signature, security category 3.
- Key agreement — X-Wing. The IETF hybrid KEM combining X25519 with ML-KEM-768 (FIPS 203), bound together by a SHA3-256 combiner. An attacker must break both to recover the key.
Microsoft.AspNetCore.Authentication.JwtBearer is the right choice for the
vast majority of JWT work today — it speaks the entire IANA JOSE algorithm
catalogue and has been hardened over a decade of production use. But
Microsoft.IdentityModel does not understand ML-DSA-65, and shimming a
post-quantum algorithm into a token validator that wasn't designed for it is
the wrong shape of problem. PostQuantum.AspNetCore bypasses that path
entirely: a fail-closed AuthenticationHandler that delegates to
PqJwtValidator and
nothing else.
Install
dotnet add package PostQuantum.AspNetCore --version 1.0.0-preview.3
Or in a .csproj:
<PackageReference Include="PostQuantum.AspNetCore" Version="1.0.0-preview.3" />
Runtime requirement: the native ML-KEM / ML-DSA primitives need an
OpenSSL build that exposes them — OpenSSL 3.5 or later on Linux, or a
recent Windows. Where they are unavailable, the underlying PostQuantum.Jwt
engine fails closed with a clear error rather than silently falling back to
weaker crypto.
60-second tour
using PostQuantum.AspNetCore;
using PostQuantum.Jwt;
using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args);
using var verificationKey = MLDsa.ImportMLDsaPublicKey(
MLDsaAlgorithm.MLDsa65,
Convert.FromBase64String(builder.Configuration["Auth:VerificationKey"]!));
builder.Services
.AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)
.AddPostQuantumJwtBearer(options =>
{
options.ValidationParameters = new PqJwtValidationParameters
{
SignatureVerificationKey = verificationKey,
ValidIssuer = builder.Configuration["Auth:Issuer"],
ValidAudience = builder.Configuration["Auth:Audience"],
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/me", (HttpContext ctx) => new
{
sub = ctx.User.FindFirst("sub")?.Value,
role = ctx.User.FindFirst("role")?.Value,
}).RequireAuthorization();
app.Run();
That's the whole integration. The handler is fail-closed by construction
(tampered, expired, or wrong-issuer tokens produce AuthenticateResult.Fail),
RequireAuthorization() returns 401 to unauthenticated callers, and standard
[Authorize(Roles = "...")] attributes work against the "role" claim.
⚠️ Before you ship this to production, add one more line for distributed replay protection — without it, captured tokens are reusable until they expire:
// dotnet add package PostQuantum.AspNetCore.RedisReplayCache builder.Services.AddPostQuantumJwtRedisReplayCache( builder.Configuration["Redis:ConnectionString"]!);See the headline section below and the full Security model for the deployment-shape matrix.
A runnable end-to-end version of this — issuer endpoint, protected endpoint,
ephemeral key pair — lives in samples/PostQuantum.AspNetCore.Demo.
dotnet run --project samples/PostQuantum.AspNetCore.Demo
# in another shell
TOKEN=$(curl -s -X POST http://localhost:5000/dev/token | jq -r .token)
curl -H "Authorization: Bearer $TOKEN" http://localhost:5000/me
A second sample — samples/PostQuantum.AspNetCore.SignalR.Demo
— exercises the OnMessageReceived event end-to-end against a real
SignalR hub with the canonical ?access_token= connection pattern,
plus an in-page browser client so the whole loop runs in one process:
dotnet run --project samples/PostQuantum.AspNetCore.SignalR.Demo
# browse to http://localhost:5050/
A third sample — samples/PostQuantum.AspNetCore.Mvc.Demo
— shows the classic controller-based ASP.NET Core MVC pattern:
[Authorize], [Authorize(Roles = "admin")], and
[Authorize(Policy = "AcmeTenant")] against PQ tokens, with an
in-page browser harness that mints and exercises tokens against
each endpoint:
dotnet run --project samples/PostQuantum.AspNetCore.Mvc.Demo
# browse to http://localhost:5100/
Usage
Sign and validate
The handler validates whatever PqJwtValidator accepts — single-key,
issuer-and-audience pinned, with optional replay defence:
builder.Services
.AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)
.AddPostQuantumJwtBearer(options =>
{
options.ValidationParameters = new PqJwtValidationParameters
{
SignatureVerificationKey = verificationKey,
ValidIssuer = "https://issuer.example",
ValidAudience = "https://api.example",
// Single-process replay defence. Swap to a Redis-backed
// IPqJwtReplayCache for a horizontally scaled deployment.
ReplayCache = new InMemoryReplayCache(),
};
});
Token minting lives in PostQuantum.Jwt itself — PqJwtBuilder — and is not
duplicated here. This package is the receiving half.
Events: enrich, observe, customize the challenge
PostQuantumJwtBearerEvents mirrors the shape of JwtBearerEvents —
four async hooks for the moments that matter:
.AddPostQuantumJwtBearer(options =>
{
options.ValidationParameters = new PqJwtValidationParameters { /* ... */ };
// Substitute a token from a non-Authorization-header source.
// SignalR's ?access_token= is the canonical use case.
options.Events.OnMessageReceived = ctx =>
{
if (ctx.HttpContext.Request.Path.StartsWithSegments("/hub"))
{
var query = ctx.HttpContext.Request.Query["access_token"].ToString();
if (!string.IsNullOrEmpty(query))
{
ctx.Token = query;
}
}
return Task.CompletedTask;
};
// Enrich the principal after a token has been successfully validated.
options.Events.OnTokenValidated = ctx =>
{
var identity = (System.Security.Claims.ClaimsIdentity)ctx.Principal.Identity!;
identity.AddClaim(new("tenant", ResolveTenant(ctx.HttpContext)));
return Task.CompletedTask;
};
// Observe (or, rarely, override) the failure outcome.
options.Events.OnAuthenticationFailed = ctx =>
{
// ctx.Exception is the PqJwtValidationException.
// Setting ctx.Result downgrades the default Fail() — usually you
// just log and let the fail-closed default stand.
return Task.CompletedTask;
};
// Customise the 401 challenge response.
options.Events.OnChallenge = ctx =>
{
if (ctx.HttpContext.Request.Path.StartsWithSegments("/api"))
{
ctx.HttpContext.Response.Headers["X-PQ-Auth"] = "required";
}
// ctx.Handled = true; suppresses the default WWW-Authenticate header.
return Task.CompletedTask;
};
});
Hook delegates default to no-ops, so leaving Events alone gives you
the same behaviour as not having the hooks at all.
Distributed replay protection with Redis ⭐ recommended for production
A captured token shouldn't be reusable. For any deployment with more than one instance, configure a distributed replay cache. The companion package ships a Redis implementation that's a one-line wireup:
dotnet add package PostQuantum.AspNetCore.RedisReplayCache --version 1.0.0-preview.3
using PostQuantum.AspNetCore.RedisReplayCache;
builder.Services
.AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)
.AddPostQuantumJwtBearer(options =>
{
options.ValidationParameters = new PqJwtValidationParameters
{
SignatureVerificationKey = verificationKey,
ValidIssuer = "https://issuer.example",
ValidAudience = "https://api.example",
};
});
// One line: registers RedisPqJwtReplayCache as a singleton, wires it
// onto the scheme's ValidationParameters.ReplayCache via PostConfigure.
builder.Services.AddPostQuantumJwtRedisReplayCache(
connectionString: builder.Configuration["Redis:ConnectionString"]!);
Under the hood: every accepted token issues a Redis SET key 1 NX PX {remaining-token-lifetime}. First use wins, replays return false
→ validator throws PqJwtValidationException → handler returns 401.
The TTL means the cache cleans itself up after token expiration.
Why this matters: without a configured replay cache, the jti
claim is carried by every token but never enforced. A captured
token is reusable until it expires. The library is opt-in on this
because single-process apps don't need a distributed cache — but for
anything multi-instance, this is the recommended production
configuration.
The bundled InMemoryReplayCache from PostQuantum.Jwt works for
single-process apps; the SECURITY-MODEL.md
documents the deployment-shape matrix in detail.
OpenTelemetry: metrics and distributed tracing
The library emits Metrics + ActivitySource under the
"PostQuantum.AspNetCore" instrumentation name. One-liner wireup:
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddMeter("PostQuantum.AspNetCore")
.AddPrometheusExporter())
.WithTracing(t => t.AddSource("PostQuantum.AspNetCore")
.AddAspNetCoreInstrumentation()
.AddOtlpExporter());
You get auth-success/failure counters, validation-latency histograms, key-ring lookup tags, and a per-validation tracing span — everything you need to build a "post-quantum auth health" dashboard.
Full signal contract in the recipes.
Issuing tokens (server-side)
Token minting lives in the engine library (PostQuantum.Jwt),
because the issuer doesn't need ASP.NET Core to mint:
using PostQuantum.Jwt;
// signingKey is the private half — load from your secret store.
string token = new PqJwtBuilder()
.WithIssuer("https://issuer.example")
.WithAudience("https://api.example")
.WithSubject("user-42")
.WithJwtId(Guid.NewGuid().ToString("N")) // for replay protection
.WithLifetime(TimeSpan.FromMinutes(15))
.WithClaim("role", "admin")
.WithKeyId("signing-key-2026-q2") // for kid rotation
.SignWith(signingKey)
.Build();
Publish the verification half (public key) via your JWKS-equivalent endpoint so resource servers can validate without sharing secrets.
Key rotation across services
Use AddPostQuantumJwtKeyRing(uri) to fetch verification keys from a
trusted HTTPS endpoint (the post-quantum analogue of JWKS). The validator
picks the right key for each incoming token from its kid header:
builder.Services
.AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)
.AddPostQuantumJwtBearer(options =>
{
options.ValidationParameters = new PqJwtValidationParameters
{
ValidIssuer = builder.Configuration["Auth:Issuer"],
ValidAudience = builder.Configuration["Auth:Audience"],
// No key here — the ring supplies it.
};
});
// Registers HttpPostQuantumJwtKeyRing as a typed HTTP client and
// post-configures it onto the named options. No BuildServiceProvider()
// dance.
builder.Services.AddPostQuantumJwtKeyRing(
new Uri(builder.Configuration["Auth:KeysEndpoint"]!));
For a non-HTTP key source (database, KMS, file), supply your own
IPostQuantumJwtKeyRing implementation and register it generically:
builder.Services.AddPostQuantumJwtKeyRing<MyDatabaseKeyRing>();
Warm the cache at startup. A cold cache means the first
authentication request pays a network round trip while every other
request waits. Register the hosted-service warmup helper to preload at
host start (and optionally on a periodic timer so removed keys drop
out without waiting for an unknown-kid miss):
builder.Services.AddPostQuantumJwtKeyRing(
new Uri(builder.Configuration["Auth:KeysEndpoint"]!));
builder.Services.AddPostQuantumJwtKeyRingWarmup(options =>
{
options.FailFastOnStartup = true; // default
options.RefreshInterval = TimeSpan.FromMinutes(15);
});
FailFastOnStartup (default true) makes a startup-time fetch failure
abort the host — strict, but matches the engine library's fail-closed
ethos. Set it to false for best-effort warmup that logs and lets the
host come up; the first cache miss will then drive a refresh as usual.
The expected key-directory document is JSON:
{ "keys": [ { "kid": "2026-q2", "alg": "ML-DSA-65", "key": "<base64>" } ] }
Entries with any other alg are ignored — the single-suite policy holds
across services.
Custom scheme name
If you already have a JwtBearer scheme on the same app (e.g. for a slow
migration), register the post-quantum scheme under its own name and route
specific endpoints to it:
builder.Services
.AddAuthentication()
.AddJwtBearer("Classical", o => { /* legacy config */ })
.AddPostQuantumJwtBearer("PostQuantum", o =>
{
o.ValidationParameters = new PqJwtValidationParameters { /* ... */ };
});
[Authorize(AuthenticationSchemes = "PostQuantum")]
public class ProtectedController : ControllerBase { /* ... */ }
Don't
AddJwtBeareralongside this on the default scheme. The standard handler will try to parse the token'salgand fail. Either useAddPostQuantumJwtBeareras your only bearer auth, or restrict each scheme to specific routes with[Authorize(AuthenticationSchemes = ...)].
Public API at a glance
| Type | Purpose |
|---|---|
PostQuantumJwtBearerExtensions |
AddPostQuantumJwtBearer(...) extension methods on AuthenticationBuilder. |
PostQuantumJwtBearerHandler |
Fail-closed AuthenticationHandler that delegates to PqJwtValidator. |
PostQuantumJwtBearerOptions |
Strongly-typed configuration: validation parameters, claim mapping, challenge details. |
PostQuantumJwtBearerDefaults |
Scheme name and Bearer constant. |
PostQuantumJwtBearerEvents |
OnMessageReceived / OnTokenValidated / OnAuthenticationFailed / OnChallenge async hooks. |
IPostQuantumJwtKeyRing |
JWKS-equivalent abstraction for kid → MLDsa resolution (sync + async). |
HttpPostQuantumJwtKeyRing |
HTTP-backed key ring with refresh, in-memory cache, atomic snapshot swap, AOT-safe JSON. |
PostQuantumJwtKeyRingExtensions |
AddPostQuantumJwtKeyRing(...) DI helpers (HTTP and generic). |
PostQuantumJwtKeyRingWarmupExtensions |
AddPostQuantumJwtKeyRingWarmup(...) — hosted-service preload + periodic refresh. |
PostQuantumJwtKeyDirectory / …KeyEntry |
DTOs for the key-directory wire format. |
Defaults and what they mean
| Setting | Default | Why |
|---|---|---|
| Scheme name | "PostQuantumJwtBearer" |
Distinct from the standard "Bearer" scheme so the two can coexist during migration. |
NameClaimType |
"sub" |
Standard JWT subject claim. The default JwtBearer value ("unique_name") is less portable. |
RoleClaimType |
"role" |
Matches common ML-DSA-issued tokens; works with [Authorize(Roles = ...)] out of the box. |
IncludeErrorDetailsInChallenge |
true |
The 401 WWW-Authenticate header carries error="invalid_token". Set to false if you'd rather not signal why. |
TimeProvider |
TimeProvider.System (inherited from AuthenticationSchemeOptions) |
Override with TimeProvider.Fake for deterministic tests. |
Compared to Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.AspNetCore.Authentication.JwtBearer is the right choice for any
JWT work that needs to interoperate with OAuth/OIDC, JWKS, the IANA JOSE
algorithm registry, or any third-party token issuer. Use it unless you have
a specific reason not to.
PostQuantum.AspNetCore is a focused, deliberately non-interoperable tool
for one problem: hybrid post-quantum JWT authentication.
| Concern | Microsoft.AspNetCore.Authentication.JwtBearer |
PostQuantum.AspNetCore |
|---|---|---|
| Algorithms | RS/PS/ES/EdDSA/HS — the full IANA catalogue. | One suite only: ML-DSA-65 for signatures; X-Wing + AES-256-GCM for encryption. |
| Quantum resistance | None of the standard algorithms are quantum-resistant. | Hybrid: classical and post-quantum, both must fall. |
| Algorithm agility | Yes — and historically the source of alg: none, RS/HS confusion, and downgrade attacks. |
No, by design. The validator does not trust the token's alg to pick a path; it accepts exactly one. |
| Standards interop | Fully IANA-registered identifiers; tokens validate in every JWT library. | Identifiers (ML-DSA-65, X-Wing) are not IANA-registered. Tokens will not validate in generic JWT tooling. |
| JWKS | First-class. | IPostQuantumJwtKeyRing + HTTP-backed implementation — JWKS-equivalent over a deliberately trivial wire format. |
| External audit | Yes — widely deployed and reviewed. | No. Preview, not audited. |
| Dependencies | Microsoft.IdentityModel.* family. |
PostQuantum.Jwt + the Microsoft.AspNetCore.App framework reference. |
| Target framework | net8 / net9 / net10. | net10.0 only (matches the engine). |
Use Microsoft.AspNetCore.Authentication.JwtBearer if you need OAuth/OIDC
interop, JWKS, multi-algorithm agility, or any standards-conformant JWT.
Use PostQuantum.AspNetCore if you specifically want hybrid post-quantum
tokens now, you control both the issuer and the verifier, and you accept
that your tokens won't validate in any other ecosystem until IANA registers
these identifiers and standard libraries catch up.
Not to be confused with…
| Package | What it is | Why it isn't this |
|---|---|---|
BouncyCastle.Cryptography |
A full-stack C# cryptography toolkit — block ciphers, public-key crypto, X.509, TLS, PKCS, OpenPGP, post-quantum primitives, and more. | A primitive library — no JWT support, no ASP.NET Core integration. PostQuantum.Jwt uses it for X25519 only; this package never touches it directly. |
liboqs / liboqs-dotnet |
Open-source post-quantum cryptography primitives (KEMs, signatures) maintained by the Open Quantum Safe project. | A primitive library. Different choice from the BCL's MLDsa/MLKem; the engine library has chosen the BCL path. |
System.Security.Cryptography (BCL) |
The .NET 10 base class library — including FIPS-validated MLDsa, MLKem, AesGcm, etc. |
The actual implementation under everything else in the diagram above. PostQuantum.AspNetCore does not reimplement any BCL primitive. |
PostQuantum.Jwt |
The engine: PqJwtBuilder to mint hybrid signed / signed-then-encrypted tokens; PqJwtValidator to verify them; the X-Wing combiner; the JWE wire format. |
The library under PostQuantum.AspNetCore. If you're not using ASP.NET Core, use this directly. |
Microsoft.AspNetCore.Authentication.JwtBearer |
Microsoft's standard JWT bearer handler. Supports every IANA JOSE algorithm. | The right choice for every JWT scenario except post-quantum. This package is the post-quantum sibling, not a replacement. |
Migrating from AddJwtBearer
Moving from Microsoft.AspNetCore.Authentication.JwtBearer is
deliberately a one-line change at the call site. The validation
model is different (we use a static key or a custom JWKS-equivalent
ring, not an OIDC Authority), but the shape is identical: same
AuthenticationBuilder, same scheme name pattern, same
[Authorize] attribute, same ClaimsPrincipal downstream. Most
controllers, policies, and middleware need no change at all.
Side-by-side
Before — classical JwtBearer against an OIDC provider:
using Microsoft.AspNetCore.Authentication.JwtBearer;
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// JwtBearer discovers signing keys from the OIDC metadata document.
options.Authority = "https://auth.example/";
options.Audience = "https://api.example/";
options.TokenValidationParameters.ValidIssuer = "https://auth.example/";
});
builder.Services.AddAuthorization();
After — post-quantum AddPostQuantumJwtBearer against your own issuer:
using PostQuantum.AspNetCore;
using PostQuantum.Jwt;
builder.Services
.AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)
.AddPostQuantumJwtBearer(options =>
{
// No OIDC discovery — supply the ML-DSA-65 verification key directly
// (or via the JWKS-equivalent IPostQuantumJwtKeyRing for rotation).
options.ValidationParameters = new PqJwtValidationParameters
{
SignatureVerificationKey = verificationKey,
ValidIssuer = "https://issuer.example",
ValidAudience = "https://api.example",
};
});
builder.Services.AddAuthorization();
Everything downstream of those lines — [Authorize],
[Authorize(Roles = "...")], policies, User.FindFirst("sub"),
HttpContext.User.IsAuthenticated — works unchanged.
What's different
| Concern | AddJwtBearer |
AddPostQuantumJwtBearer |
|---|---|---|
| Algorithms accepted | Full IANA catalogue (RS/PS/ES/EdDSA/HS). | Exactly one suite: ML-DSA-65. |
| Key source | Authority (OIDC discovery) or IssuerSigningKey. |
SignatureVerificationKey (static) or IPostQuantumJwtKeyRing (dynamic). |
| Identity provider integration | Auth0, IdentityServer, Microsoft Entra, etc. | You issue tokens via PqJwtBuilder. Not OIDC-compatible. |
| Token size | ~200 bytes (HMAC) → ~1 KB (RSA). | ~4.5 KB (ML-DSA-65 signature is 3,309 bytes). |
| Algorithm agility | Yes (and historically a source of CVEs). | No, by design. Token's alg doesn't pick a code path. |
| Replay protection | Not built-in. | Built-in via IPqJwtReplayCache + Redis companion (opt-in). |
| Standards interop | Tokens validate in any JWT library. | Tokens are non-interoperable until IANA registers ML-DSA-65. |
| Production maturity | Yes — decade-hardened. | Preview — not audited, not for production. |
Run both during migration
You don't have to flip a switch — register both schemes and route specific endpoints to each:
builder.Services
.AddAuthentication()
.AddJwtBearer("Classical", o => { o.Authority = "https://auth.example/"; })
.AddPostQuantumJwtBearer("PostQuantum", o =>
{
o.ValidationParameters = new PqJwtValidationParameters { /* ... */ };
});
[Authorize(AuthenticationSchemes = "PostQuantum")]
public class PostQuantumOnlyController : ControllerBase { }
[Authorize(AuthenticationSchemes = "Classical,PostQuantum")]
public class EitherWorksController : ControllerBase { }
See docs/RECIPES.md § 7
for the full coexistence pattern.
Migrating from PostQuantum.Jwt.AspNetCore
If you're on the legacy PostQuantum.Jwt.AspNetCore companion
package (which shipped from the engine repository), PostQuantum.AspNetCore
is its renamed, repackaged successor. Same engine, cleaner
naming, its own release cadence. The mapping is mechanical
(AddPqJwtBearer → AddPostQuantumJwtBearer, PqJwtBearer* →
PostQuantumJwtBearer*, IPqJwtKeyRing → IPostQuantumJwtKeyRing),
and tokens minted by either package validate in the other.
See docs/MIGRATION.md for the diff-style guide.
Security posture
The short version, honestly. The full security contract — what the
library protects, what it does NOT protect, the replay-protection
deployment matrix, the key-rotation cadence, and the fail-closed
contract enumerated as a logical conjunction — lives in
docs/SECURITY-MODEL.md. Read that before
depending on this for anything that matters.
What you get
- Fail-closed validation. Bad signature, tampered ciphertext, expired
or not-yet-valid token, wrong issuer/audience, missing
exp, missingalg, or analgwe don't expect — every one of those throws insidePqJwtValidator, and the handler turns it intoAuthenticateResult.Fail. There is noalg: none, no unsigned path, no silent downgrade, and no exception class fromValidate()escapes as a500. - Native post-quantum primitives. ML-DSA-65 and ML-KEM-768 are the FIPS-validated .NET 10 BCL implementations, not a re-implementation.
- Hybrid by construction (for encrypted tokens). Confidentiality stays secure unless both X25519 and ML-KEM-768 fall.
- Strict, small-surface defaults. Expiration is required, clock skew
is a modest 60 seconds, only the exact post-quantum algorithms are
accepted, the bearer prefix is matched case-insensitively per RFC 6750,
and the
WWW-Authenticaterealm is RFC 7235 quoted-string escaped.
What you must know
- Not audited. No third party has reviewed the design or implementation.
- Non-standard identifiers.
alg/encvalues (ML-DSA-65,X-Wing) are not IANA-registered. Tokens are intentionally not interoperable with generic JWT tooling. - Preview. Treat the API and wire format as unstable until 1.0.
- ⚠️ Replay defence is opt-in. Without a configured
IPqJwtReplayCache, captured tokens are reusable until they expire. For multi-instance production, usePostQuantum.AspNetCore.RedisReplayCache— see the headline section above.
Full detail in docs/SECURITY-MODEL.md,
SECURITY.md, and KNOWN-GAPS.md.
Compatibility
| Surface | Supported |
|---|---|
| Target framework | net10.0 |
| ASP.NET Core | 10.x via <FrameworkReference Include="Microsoft.AspNetCore.App" /> |
| Languages | C# 13 |
| Operating system | Windows, Linux, macOS — wherever .NET 10 runs with an ML-KEM / ML-DSA-capable OpenSSL. On Linux that means OpenSSL 3.5 or later. |
| AOT / trimming | IsAotCompatible=true, IsTrimmable=true. The HTTP key-ring JSON path is source-generated. |
Building from source
dotnet build # zero warnings (compiler warnings are errors)
dotnet test # 31 tests, zero skips on PQ-capable hosts
dotnet format # apply the .editorconfig style
dotnet run --project samples/PostQuantum.AspNetCore.Demo
The test suite is Microsoft.AspNetCore.Mvc.Testing-backed and exercises
the fail-closed contract end-to-end: valid token → 200 OK with the
expected ClaimsPrincipal, every tampered/expired/wrong-issuer/wrong-audience
case → 401 Unauthorized, plus assertions on each of the three event hooks.
Tests that exercise the native ML-DSA primitives skip themselves
with a stated reason (PqcFactAttribute) on hosts where the BCL
primitives aren't available; both CI lanes (Windows native + Linux with
OpenSSL 3.5+ via conda-forge) fail the run if any test reports skipped.
Contributing
Issues and pull requests are welcome. Before opening a PR:
- Run
dotnet buildanddotnet test— both must be green, with zero warnings (the build treats compiler warnings as errors). - Keep the discipline in
CLAUDE.md: honesty over polish, fail-closed always, no rolled-your-own crypto, native BCL first. - Security-sensitive changes should land alongside a test that locks in the fail-closed behaviour.
Reporting a vulnerability: please do not open a public issue. Use
GitHub's Report a vulnerability button on the repository, or follow the
process in SECURITY.md.
License
MIT.
About this library
This library was created by a human developer working in close collaboration with Claude, Gemini, Grok, and ChatGPT. The vision, direction, architecture decisions, and final curation were mine. The goal was simple: build something the .NET ecosystem genuinely needs for the post-quantum era.
To God be the glory — 1 Corinthians 10:31.
| 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
- PostQuantum.Jwt (>= 1.0.0-preview.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on PostQuantum.AspNetCore:
| Package | Downloads |
|---|---|
|
PostQuantum.AspNetCore.RedisReplayCache
A Redis-backed implementation of PostQuantum.Jwt's IPqJwtReplayCache, intended for horizontally-scaled deployments of PostQuantum.AspNetCore. Uses StackExchange.Redis SET NX with TTL = remaining token lifetime, so single-use jti enforcement coordinates across all instances. Companion to PostQuantum.AspNetCore. Preview release — not for production use. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.3 | 37 | 6/2/2026 |
| 1.0.0-preview.2 | 56 | 5/31/2026 |
| 1.0.0-preview.1 | 53 | 5/31/2026 |
1.0.0-preview.3: Suite-reconciliation + integration-hardening release. (1) Suite version reconciliation — PostQuantum.Jwt engine dependency moves to 1.0.0-preview.1 to align with the wider PostQuantum.* ecosystem; no source-level API change vs 1.0.0-preview.2. (2) Trust-root documentation — SECURITY.md gains an explicit "Trust root: the HTTP key directory" section telling operators that the HTTPS key-directory endpoint is the root of trust for token validation and SHOULD be protected with certificate pinning or a hardened HttpClient. (3) AddPostQuantumJwtKeyRing(...) gains an optional configureHttpClient hook so wiring a pinned HttpMessageHandler is the obvious path, not a fork. (4) Bounded key-directory response — the DI helper now caps the typed HttpClient at MaxResponseContentBufferSize=1MB; manual constructors get an optional maxResponseBytes parameter (default unchanged). (5) Unknown-kid flood mitigation — Resolve now short-circuits via a bounded short-TTL negative cache before the sync-over-async bridge; a kid which becomes valid is wrongly rejected for at most 10 seconds, matching the pre-existing forced-refresh throttle (no regression to that bound). No change to fail-closed behaviour, no change to the single-suite policy. Full suite green. See KNOWN-GAPS.md and VERSION-RECONCILIATION.md. Full notes: https://github.com/systemslibrarian/postquantum-aspnetcore/blob/main/CHANGELOG.md.