{"id":50486947,"url":"https://github.com/systemslibrarian/postquantum-aspnetcore","last_synced_at":"2026-06-01T23:02:58.615Z","repository":{"id":361513060,"uuid":"1254740064","full_name":"systemslibrarian/postquantum-aspnetcore","owner":"systemslibrarian","description":"ASP.NET Core integration for PostQuantum.Jwt — easy quantum-resistant JWT authentication with AddPostQuantumJwtBearer().","archived":false,"fork":false,"pushed_at":"2026-05-31T01:37:55.000Z","size":131,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-31T02:25:36.701Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/systemslibrarian.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T00:17:54.000Z","updated_at":"2026-05-31T01:38:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/systemslibrarian/postquantum-aspnetcore","commit_stats":null,"previous_names":["systemslibrarian/postquantum-aspnetcore"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/systemslibrarian/postquantum-aspnetcore","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-aspnetcore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-aspnetcore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-aspnetcore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-aspnetcore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/systemslibrarian","download_url":"https://codeload.github.com/systemslibrarian/postquantum-aspnetcore/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-aspnetcore/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33797128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T23:02:57.134Z","updated_at":"2026-06-01T23:02:58.592Z","avatar_url":"https://github.com/systemslibrarian.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PostQuantum.AspNetCore\n\n[![NuGet](https://img.shields.io/nuget/vpre/PostQuantum.AspNetCore?label=nuget\u0026color=blue)](https://www.nuget.org/packages/PostQuantum.AspNetCore)\n[![CI](https://github.com/systemslibrarian/postquantum-aspnetcore/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/systemslibrarian/postquantum-aspnetcore/actions/workflows/ci.yml)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)\n\n**The high-level ASP.NET Core integration for post-quantum JWT\nauthentication.** Add one line — `AddPostQuantumJwtBearer(…)` — and\nhybrid ML-DSA-65 + X-Wing tokens authenticate through the standard\n`AuthenticationBuilder` exactly the way `AddJwtBearer` always has.\n`[Authorize]` attributes, policies, role checks, claims, middleware —\neverything downstream of the wireup works **unchanged**, because the\nhandler emits a real `ClaimsPrincipal`.\n\nBuilt on [`PostQuantum.Jwt`](https://github.com/systemslibrarian/postquantum-jwt)\nand the native .NET 10 BCL post-quantum primitives. Fail-closed by\nconstruction. Small surface. Honest about its limits.\n\n\u003e **What this package *is*: a thin, opinionated **application layer** for\n\u003e ASP.NET Core authentication.** Extension methods, an\n\u003e `AuthenticationHandler`, event hooks, a JWKS-equivalent key ring, a\n\u003e hosted-service warmup, and metrics + tracing — all the wiring you'd\n\u003e otherwise write yourself to make post-quantum JWTs feel native to\n\u003e `AddAuthentication`.\n\u003e\n\u003e **What this package is *not*: a cryptography library.** No\n\u003e implementation of ML-DSA, ML-KEM, X25519, AES-GCM, or SHA-3 lives in\n\u003e here. We don't compete with [BouncyCastle](https://www.bouncycastle.org/csharp/),\n\u003e liboqs, or `System.Security.Cryptography`. The actual signing,\n\u003e verification, key encapsulation, and content encryption all happen\n\u003e inside [`PostQuantum.Jwt`](https://github.com/systemslibrarian/postquantum-jwt),\n\u003e which in turn uses the FIPS-validated .NET 10 BCL post-quantum\n\u003e primitives (with BouncyCastle for the one piece the BCL doesn't ship:\n\u003e X25519). Think of `PostQuantum.AspNetCore` as **the** `AddJwtBearer`\n\u003e **equivalent** that knows the right things about ML-DSA-65 — not a\n\u003e reinvention of the crypto stack underneath.\n\n\u003e **Status — `1.0.0-preview.2`.** Preview software. Not for production use.\n\u003e The API may change before 1.0, and the underlying cryptographic construction\n\u003e has not been independently audited. Read [`KNOWN-GAPS.md`](KNOWN-GAPS.md)\n\u003e before depending on this for anything that matters.\n\n## Highlights\n\n- **One-line wireup** — `AddPostQuantumJwtBearer(…)` slots into the\n  standard `AuthenticationBuilder` exactly like `AddJwtBearer`.\n- **Fail-closed by construction** — every validation failure becomes\n  `401`. No `alg: none`, no algorithm fallback, no degraded path.\n- **Distributed replay protection** — single-use `jti` enforcement\n  across your fleet via the [`PostQuantum.AspNetCore.RedisReplayCache`](src/PostQuantum.AspNetCore.RedisReplayCache)\n  companion package (SET NX + remaining-token-TTL).\n- **JWKS-equivalent key rotation** — `IPostQuantumJwtKeyRing` with an\n  HTTP-backed implementation, atomic snapshot swap on refresh,\n  unknown-`kid` throttling, hosted-service startup warmup.\n- **Four event hooks** — `OnMessageReceived` (SignalR-style alternate\n  token transports), `OnTokenValidated` (enrich principal),\n  `OnAuthenticationFailed`, `OnChallenge`.\n- **First-class observability** — `System.Diagnostics.Metrics` + an\n  `ActivitySource` for OpenTelemetry / Prometheus / Application\n  Insights.\n- **AOT-compatible** — `IsAotCompatible=true`, verified end-to-end in\n  CI on Linux, Windows, and macOS.\n- **Honest about limits** — preview status, non-IANA algorithm\n  identifiers, no independent audit, every gap tracked in\n  [`KNOWN-GAPS.md`](KNOWN-GAPS.md).\n\n\u003e **In a hurry?** Jump straight to:\n\u003e\n\u003e - **[Getting started](docs/GETTING-STARTED.md)** — zero to working PQ API in 10 minutes.\n\u003e - **[Migrating from `AddJwtBearer`](#migrating-from-addjwtbearer)** — side-by-side diff.\n\u003e - **[Security model](docs/SECURITY-MODEL.md)** — what the library protects, what it doesn't, replay-protection requirements.\n\u003e - **[Recipes](docs/RECIPES.md)** — copy-paste-able scenarios: Redis replay, OpenTelemetry, SignalR, multi-tenant, multi-scheme, Swagger, Docker/K8s.\n\u003e - **[FAQ](docs/FAQ.md)** — should I use this in production? how big are tokens? does it work with Auth0? — and 15 more.\n\u003e - **[Production checklist](docs/PRODUCTION-CHECKLIST.md)** — before user traffic hits.\n\n---\n\n## Where does this fit in the stack?\n\n```\n┌──────────────────────────────────────────────────────────────────────┐\n│  Your ASP.NET Core app                                               │\n│  builder.Services.AddAuthentication().AddPostQuantumJwtBearer(...)   │\n├──────────────────────────────────────────────────────────────────────┤\n│  PostQuantum.AspNetCore                                  (this lib)  │\n│  · AuthenticationHandler + options + 4 event hooks                   │\n│  · IPostQuantumJwtKeyRing (JWKS-equivalent)                          │\n│  · Hosted-service warmup, metrics, tracing                           │\n├──────────────────────────────────────────────────────────────────────┤\n│  PostQuantum.Jwt                          (the engine, separate pkg) │\n│  · PqJwtBuilder / PqJwtValidator                                     │\n│  · X-Wing combiner, JWE wire format, replay cache                    │\n├──────────────────────────────────────────────────────────────────────┤\n│  Crypto primitives                                  (not this lib)   │\n│  · System.Security.Cryptography.MLDsa / MLKem  (.NET 10 BCL)         │\n│  · BouncyCastle.Cryptography                   (X25519 only)         │\n└──────────────────────────────────────────────────────────────────────┘\n```\n\nThis library sits at the **top** of that stack — the application\nintegration layer. It does **no cryptography of its own**. If you're\nlooking for raw ML-DSA, ML-KEM, X25519, or AES-GCM, those live in the\n.NET BCL and BouncyCastle and we are happy customers, not competitors.\n\n## Table of contents\n\n- [Where does this fit in the stack?](#where-does-this-fit-in-the-stack)\n- [Why](#why)\n- [Install](#install)\n- [60-second tour](#60-second-tour)\n- [Usage](#usage)\n  - [Sign and validate](#sign-and-validate)\n  - [Events: enrich, observe, customize the challenge](#events-enrich-observe-customize-the-challenge)\n  - [Distributed replay protection with Redis](#distributed-replay-protection-with-redis)\n  - [OpenTelemetry: metrics and distributed tracing](#opentelemetry-metrics-and-distributed-tracing)\n  - [Issuing tokens (server-side)](#issuing-tokens-server-side)\n  - [Key rotation across services](#key-rotation-across-services)\n  - [Custom scheme name](#custom-scheme-name)\n- [Sample apps](#sample-apps)\n- [Public API at a glance](#public-api-at-a-glance)\n- [Defaults and what they mean](#defaults-and-what-they-mean)\n- [Compared to `Microsoft.AspNetCore.Authentication.JwtBearer`](#compared-to-microsoftaspnetcoreauthenticationjwtbearer)\n- [Migrating from `AddJwtBearer`](#migrating-from-addjwtbearer)\n- [Migrating from `PostQuantum.Jwt.AspNetCore`](#migrating-from-postquantumjwtaspnetcore)\n- [Security posture](#security-posture)\n- [Compatibility](#compatibility)\n- [Building from source](#building-from-source)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Why\n\n**Why a separate package, when you could just call `PostQuantum.Jwt`\nyourself from your ASP.NET Core app?** Because authentication wiring is\nwhere the bugs live. Token retrieval from `Authorization` (or `?access_token=`\nfor SignalR), case-insensitive `Bearer` prefix matching, the\n`WWW-Authenticate` challenge response with RFC-compliant realm escaping,\nevent hooks for principal enrichment, key-ring rotation, hosted-service\ncache warmup, fail-closed handling of every exception path, metrics for\nops dashboards, distributed-tracing spans — `Microsoft.AspNetCore.Authentication.JwtBearer`\ndoes all of that for the *classical* algorithms. This library does it\nfor `ML-DSA-65`. **You shouldn't have to write the wiring yourself.**\n\n**Why post-quantum at all?**\nA cryptographically relevant quantum computer would break the elliptic-curve\nmath behind every JWT signature in production today (EdDSA, ECDSA, RSA). Pure\npost-quantum schemes are new and comparatively under-attacked. **Hybrid** hedges\nboth at once:\n\n- **Signatures — ML-DSA-65** (FIPS 204). NIST-standardised lattice signature,\n  security category 3.\n- **Key agreement — X-Wing.** The IETF hybrid KEM combining **X25519** with\n  **ML-KEM-768** (FIPS 203), bound together by a SHA3-256 combiner. An\n  attacker must break *both* to recover the key.\n\n`Microsoft.AspNetCore.Authentication.JwtBearer` is the right choice for the\nvast majority of JWT work today — it speaks the entire IANA JOSE algorithm\ncatalogue and has been hardened over a decade of production use. But\n`Microsoft.IdentityModel` does **not** understand `ML-DSA-65`, and shimming a\npost-quantum algorithm into a token validator that wasn't designed for it is\nthe wrong shape of problem. `PostQuantum.AspNetCore` bypasses that path\nentirely: a fail-closed `AuthenticationHandler` that delegates to\n[`PqJwtValidator`](https://github.com/systemslibrarian/postquantum-jwt) and\nnothing else.\n\n---\n\n## Install\n\n```bash\ndotnet add package PostQuantum.AspNetCore --version 1.0.0-preview.2\n```\n\nOr in a `.csproj`:\n\n```xml\n\u003cPackageReference Include=\"PostQuantum.AspNetCore\" Version=\"1.0.0-preview.2\" /\u003e\n```\n\n**Runtime requirement:** the native ML-KEM / ML-DSA primitives need an\nOpenSSL build that exposes them — **OpenSSL 3.5 or later** on Linux, or a\nrecent Windows. Where they are unavailable, the underlying `PostQuantum.Jwt`\nengine fails closed with a clear error rather than silently falling back to\nweaker crypto.\n\n---\n\n## 60-second tour\n\n```csharp\nusing PostQuantum.AspNetCore;\nusing PostQuantum.Jwt;\nusing System.Security.Cryptography;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nusing var verificationKey = MLDsa.ImportMLDsaPublicKey(\n    MLDsaAlgorithm.MLDsa65,\n    Convert.FromBase64String(builder.Configuration[\"Auth:VerificationKey\"]!));\n\nbuilder.Services\n    .AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)\n    .AddPostQuantumJwtBearer(options =\u003e\n    {\n        options.ValidationParameters = new PqJwtValidationParameters\n        {\n            SignatureVerificationKey = verificationKey,\n            ValidIssuer   = builder.Configuration[\"Auth:Issuer\"],\n            ValidAudience = builder.Configuration[\"Auth:Audience\"],\n        };\n    });\nbuilder.Services.AddAuthorization();\n\nvar app = builder.Build();\napp.UseAuthentication();\napp.UseAuthorization();\n\napp.MapGet(\"/me\", (HttpContext ctx) =\u003e new\n{\n    sub  = ctx.User.FindFirst(\"sub\")?.Value,\n    role = ctx.User.FindFirst(\"role\")?.Value,\n}).RequireAuthorization();\n\napp.Run();\n```\n\nThat's the whole integration. The handler is fail-closed by construction\n(tampered, expired, or wrong-issuer tokens produce `AuthenticateResult.Fail`),\n`RequireAuthorization()` returns 401 to unauthenticated callers, and standard\n`[Authorize(Roles = \"...\")]` attributes work against the `\"role\"` claim.\n\n\u003e **⚠️ Before you ship this to production**, add one more line for\n\u003e distributed replay protection — without it, captured tokens are\n\u003e reusable until they expire:\n\u003e\n\u003e ```csharp\n\u003e // dotnet add package PostQuantum.AspNetCore.RedisReplayCache\n\u003e builder.Services.AddPostQuantumJwtRedisReplayCache(\n\u003e     builder.Configuration[\"Redis:ConnectionString\"]!);\n\u003e ```\n\u003e\n\u003e See the [headline section](#distributed-replay-protection-with-redis--recommended-for-production)\n\u003e below and the full [Security model](docs/SECURITY-MODEL.md) for the\n\u003e deployment-shape matrix.\n\nA runnable end-to-end version of this — issuer endpoint, protected endpoint,\nephemeral key pair — lives in [`samples/PostQuantum.AspNetCore.Demo`](samples/PostQuantum.AspNetCore.Demo).\n\n```bash\ndotnet run --project samples/PostQuantum.AspNetCore.Demo\n# in another shell\nTOKEN=$(curl -s -X POST http://localhost:5000/dev/token | jq -r .token)\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5000/me\n```\n\nA second sample — [`samples/PostQuantum.AspNetCore.SignalR.Demo`](samples/PostQuantum.AspNetCore.SignalR.Demo)\n— exercises the `OnMessageReceived` event end-to-end against a real\nSignalR hub with the canonical `?access_token=` connection pattern,\nplus an in-page browser client so the whole loop runs in one process:\n\n```bash\ndotnet run --project samples/PostQuantum.AspNetCore.SignalR.Demo\n# browse to http://localhost:5050/\n```\n\nA third sample — [`samples/PostQuantum.AspNetCore.Mvc.Demo`](samples/PostQuantum.AspNetCore.Mvc.Demo)\n— shows the classic controller-based ASP.NET Core MVC pattern:\n`[Authorize]`, `[Authorize(Roles = \"admin\")]`, and\n`[Authorize(Policy = \"AcmeTenant\")]` against PQ tokens, with an\nin-page browser harness that mints and exercises tokens against\neach endpoint:\n\n```bash\ndotnet run --project samples/PostQuantum.AspNetCore.Mvc.Demo\n# browse to http://localhost:5100/\n```\n\n---\n\n## Usage\n\n### Sign and validate\n\nThe handler validates whatever `PqJwtValidator` accepts — single-key,\nissuer-and-audience pinned, with optional replay defence:\n\n```csharp\nbuilder.Services\n    .AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)\n    .AddPostQuantumJwtBearer(options =\u003e\n    {\n        options.ValidationParameters = new PqJwtValidationParameters\n        {\n            SignatureVerificationKey = verificationKey,\n            ValidIssuer   = \"https://issuer.example\",\n            ValidAudience = \"https://api.example\",\n            // Single-process replay defence. Swap to a Redis-backed\n            // IPqJwtReplayCache for a horizontally scaled deployment.\n            ReplayCache = new InMemoryReplayCache(),\n        };\n    });\n```\n\nToken minting lives in `PostQuantum.Jwt` itself — `PqJwtBuilder` — and is not\nduplicated here. This package is the *receiving* half.\n\n### Events: enrich, observe, customize the challenge\n\n`PostQuantumJwtBearerEvents` mirrors the shape of `JwtBearerEvents` —\nfour async hooks for the moments that matter:\n\n```csharp\n.AddPostQuantumJwtBearer(options =\u003e\n{\n    options.ValidationParameters = new PqJwtValidationParameters { /* ... */ };\n\n    // Substitute a token from a non-Authorization-header source.\n    // SignalR's ?access_token= is the canonical use case.\n    options.Events.OnMessageReceived = ctx =\u003e\n    {\n        if (ctx.HttpContext.Request.Path.StartsWithSegments(\"/hub\"))\n        {\n            var query = ctx.HttpContext.Request.Query[\"access_token\"].ToString();\n            if (!string.IsNullOrEmpty(query))\n            {\n                ctx.Token = query;\n            }\n        }\n\n        return Task.CompletedTask;\n    };\n\n    // Enrich the principal after a token has been successfully validated.\n    options.Events.OnTokenValidated = ctx =\u003e\n    {\n        var identity = (System.Security.Claims.ClaimsIdentity)ctx.Principal.Identity!;\n        identity.AddClaim(new(\"tenant\", ResolveTenant(ctx.HttpContext)));\n        return Task.CompletedTask;\n    };\n\n    // Observe (or, rarely, override) the failure outcome.\n    options.Events.OnAuthenticationFailed = ctx =\u003e\n    {\n        // ctx.Exception is the PqJwtValidationException.\n        // Setting ctx.Result downgrades the default Fail() — usually you\n        // just log and let the fail-closed default stand.\n        return Task.CompletedTask;\n    };\n\n    // Customise the 401 challenge response.\n    options.Events.OnChallenge = ctx =\u003e\n    {\n        if (ctx.HttpContext.Request.Path.StartsWithSegments(\"/api\"))\n        {\n            ctx.HttpContext.Response.Headers[\"X-PQ-Auth\"] = \"required\";\n        }\n\n        // ctx.Handled = true; suppresses the default WWW-Authenticate header.\n        return Task.CompletedTask;\n    };\n});\n```\n\nHook delegates default to no-ops, so leaving `Events` alone gives you\nthe same behaviour as not having the hooks at all.\n\n### Distributed replay protection with Redis ⭐ recommended for production\n\nA captured token shouldn't be reusable. **For any deployment with\nmore than one instance, configure a distributed replay cache.** The\ncompanion package ships a Redis implementation that's a one-line\nwireup:\n\n```bash\ndotnet add package PostQuantum.AspNetCore.RedisReplayCache --version 1.0.0-preview.2\n```\n\n```csharp\nusing PostQuantum.AspNetCore.RedisReplayCache;\n\nbuilder.Services\n    .AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)\n    .AddPostQuantumJwtBearer(options =\u003e\n    {\n        options.ValidationParameters = new PqJwtValidationParameters\n        {\n            SignatureVerificationKey = verificationKey,\n            ValidIssuer   = \"https://issuer.example\",\n            ValidAudience = \"https://api.example\",\n        };\n    });\n\n// One line: registers RedisPqJwtReplayCache as a singleton, wires it\n// onto the scheme's ValidationParameters.ReplayCache via PostConfigure.\nbuilder.Services.AddPostQuantumJwtRedisReplayCache(\n    connectionString: builder.Configuration[\"Redis:ConnectionString\"]!);\n```\n\nUnder the hood: every accepted token issues a Redis `SET key 1 NX PX\n{remaining-token-lifetime}`. First use wins, replays return `false`\n→ validator throws `PqJwtValidationException` → handler returns `401`.\nThe TTL means the cache cleans itself up after token expiration.\n\n**Why this matters:** without a configured replay cache, the `jti`\nclaim is carried by every token but **never enforced**. A captured\ntoken is reusable until it expires. The library is opt-in on this\nbecause single-process apps don't need a distributed cache — but for\nanything multi-instance, **this is the recommended production\nconfiguration**.\n\nThe bundled `InMemoryReplayCache` from `PostQuantum.Jwt` works for\nsingle-process apps; the [`SECURITY-MODEL.md`](docs/SECURITY-MODEL.md#deployment-shape-matrix)\ndocuments the deployment-shape matrix in detail.\n\n### OpenTelemetry: metrics and distributed tracing\n\nThe library emits Metrics + ActivitySource under the\n`\"PostQuantum.AspNetCore\"` instrumentation name. One-liner wireup:\n\n```csharp\nusing OpenTelemetry.Metrics;\nusing OpenTelemetry.Trace;\n\nbuilder.Services.AddOpenTelemetry()\n    .WithMetrics(m =\u003e m.AddMeter(\"PostQuantum.AspNetCore\")\n                       .AddPrometheusExporter())\n    .WithTracing(t =\u003e t.AddSource(\"PostQuantum.AspNetCore\")\n                       .AddAspNetCoreInstrumentation()\n                       .AddOtlpExporter());\n```\n\nYou get auth-success/failure counters, validation-latency\nhistograms, key-ring lookup tags, and a per-validation tracing\nspan — everything you need to build a \"post-quantum auth health\"\ndashboard.\n\n[Full signal contract](docs/RECIPES.md#8-opentelemetry-metrics-and-distributed-tracing)\nin the recipes.\n\n### Issuing tokens (server-side)\n\nToken minting lives in the engine library (`PostQuantum.Jwt`),\nbecause the issuer doesn't need ASP.NET Core to mint:\n\n```csharp\nusing PostQuantum.Jwt;\n\n// signingKey is the private half — load from your secret store.\nstring token = new PqJwtBuilder()\n    .WithIssuer(\"https://issuer.example\")\n    .WithAudience(\"https://api.example\")\n    .WithSubject(\"user-42\")\n    .WithJwtId(Guid.NewGuid().ToString(\"N\"))      // for replay protection\n    .WithLifetime(TimeSpan.FromMinutes(15))\n    .WithClaim(\"role\", \"admin\")\n    .WithKeyId(\"signing-key-2026-q2\")              // for kid rotation\n    .SignWith(signingKey)\n    .Build();\n```\n\nPublish the **verification** half (public key) via your\nJWKS-equivalent endpoint so resource servers can validate without\nsharing secrets.\n\n### Key rotation across services\n\nUse `AddPostQuantumJwtKeyRing(uri)` to fetch verification keys from a\ntrusted HTTPS endpoint (the post-quantum analogue of JWKS). The validator\npicks the right key for each incoming token from its `kid` header:\n\n```csharp\nbuilder.Services\n    .AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)\n    .AddPostQuantumJwtBearer(options =\u003e\n    {\n        options.ValidationParameters = new PqJwtValidationParameters\n        {\n            ValidIssuer   = builder.Configuration[\"Auth:Issuer\"],\n            ValidAudience = builder.Configuration[\"Auth:Audience\"],\n            // No key here — the ring supplies it.\n        };\n    });\n\n// Registers HttpPostQuantumJwtKeyRing as a typed HTTP client and\n// post-configures it onto the named options. No BuildServiceProvider()\n// dance.\nbuilder.Services.AddPostQuantumJwtKeyRing(\n    new Uri(builder.Configuration[\"Auth:KeysEndpoint\"]!));\n```\n\nFor a non-HTTP key source (database, KMS, file), supply your own\n`IPostQuantumJwtKeyRing` implementation and register it generically:\n\n```csharp\nbuilder.Services.AddPostQuantumJwtKeyRing\u003cMyDatabaseKeyRing\u003e();\n```\n\n**Warm the cache at startup.** A cold cache means the first\nauthentication request pays a network round trip while every other\nrequest waits. Register the hosted-service warmup helper to preload at\nhost start (and optionally on a periodic timer so removed keys drop\nout without waiting for an unknown-`kid` miss):\n\n```csharp\nbuilder.Services.AddPostQuantumJwtKeyRing(\n    new Uri(builder.Configuration[\"Auth:KeysEndpoint\"]!));\n\nbuilder.Services.AddPostQuantumJwtKeyRingWarmup(options =\u003e\n{\n    options.FailFastOnStartup = true;                // default\n    options.RefreshInterval   = TimeSpan.FromMinutes(15);\n});\n```\n\n`FailFastOnStartup` (default `true`) makes a startup-time fetch failure\nabort the host — strict, but matches the engine library's fail-closed\nethos. Set it to `false` for best-effort warmup that logs and lets the\nhost come up; the first cache miss will then drive a refresh as usual.\n\nThe expected key-directory document is JSON:\n\n```json\n{ \"keys\": [ { \"kid\": \"2026-q2\", \"alg\": \"ML-DSA-65\", \"key\": \"\u003cbase64\u003e\" } ] }\n```\n\nEntries with any other `alg` are ignored — the single-suite policy holds\nacross services.\n\n### Custom scheme name\n\nIf you already have a `JwtBearer` scheme on the same app (e.g. for a slow\nmigration), register the post-quantum scheme under its own name and route\nspecific endpoints to it:\n\n```csharp\nbuilder.Services\n    .AddAuthentication()\n    .AddJwtBearer(\"Classical\", o =\u003e { /* legacy config */ })\n    .AddPostQuantumJwtBearer(\"PostQuantum\", o =\u003e\n    {\n        o.ValidationParameters = new PqJwtValidationParameters { /* ... */ };\n    });\n```\n\n```csharp\n[Authorize(AuthenticationSchemes = \"PostQuantum\")]\npublic class ProtectedController : ControllerBase { /* ... */ }\n```\n\n\u003e **Don't `AddJwtBearer` *alongside* this on the default scheme.** The\n\u003e standard handler will try to parse the token's `alg` and fail. Either use\n\u003e `AddPostQuantumJwtBearer` as your only bearer auth, or restrict each scheme\n\u003e to specific routes with `[Authorize(AuthenticationSchemes = ...)]`.\n\n---\n\n## Public API at a glance\n\n| Type                                     | Purpose                                                                |\n|------------------------------------------|------------------------------------------------------------------------|\n| `PostQuantumJwtBearerExtensions`         | `AddPostQuantumJwtBearer(...)` extension methods on `AuthenticationBuilder`. |\n| `PostQuantumJwtBearerHandler`            | Fail-closed `AuthenticationHandler` that delegates to `PqJwtValidator`. |\n| `PostQuantumJwtBearerOptions`            | Strongly-typed configuration: validation parameters, claim mapping, challenge details. |\n| `PostQuantumJwtBearerDefaults`           | Scheme name and `Bearer` constant.                                     |\n| `PostQuantumJwtBearerEvents`             | `OnMessageReceived` / `OnTokenValidated` / `OnAuthenticationFailed` / `OnChallenge` async hooks. |\n| `IPostQuantumJwtKeyRing`                 | JWKS-equivalent abstraction for `kid → MLDsa` resolution (sync + async). |\n| `HttpPostQuantumJwtKeyRing`              | HTTP-backed key ring with refresh, in-memory cache, atomic snapshot swap, AOT-safe JSON. |\n| `PostQuantumJwtKeyRingExtensions`        | `AddPostQuantumJwtKeyRing(...)` DI helpers (HTTP and generic).         |\n| `PostQuantumJwtKeyRingWarmupExtensions`  | `AddPostQuantumJwtKeyRingWarmup(...)` — hosted-service preload + periodic refresh. |\n| `PostQuantumJwtKeyDirectory` / `…KeyEntry` | DTOs for the key-directory wire format.                              |\n\n---\n\n## Defaults and what they mean\n\n| Setting                          | Default                                              | Why                                                                                  |\n|----------------------------------|------------------------------------------------------|--------------------------------------------------------------------------------------|\n| Scheme name                      | `\"PostQuantumJwtBearer\"`                             | Distinct from the standard `\"Bearer\"` scheme so the two can coexist during migration. |\n| `NameClaimType`                  | `\"sub\"`                                              | Standard JWT subject claim. The default `JwtBearer` value (`\"unique_name\"`) is less portable. |\n| `RoleClaimType`                  | `\"role\"`                                             | Matches common ML-DSA-issued tokens; works with `[Authorize(Roles = ...)]` out of the box. |\n| `IncludeErrorDetailsInChallenge` | `true`                                               | The 401 `WWW-Authenticate` header carries `error=\"invalid_token\"`. Set to `false` if you'd rather not signal why. |\n| `TimeProvider`                   | `TimeProvider.System` (inherited from `AuthenticationSchemeOptions`) | Override with `TimeProvider.Fake` for deterministic tests. |\n\n---\n\n## Compared to `Microsoft.AspNetCore.Authentication.JwtBearer`\n\n`Microsoft.AspNetCore.Authentication.JwtBearer` is the right choice for any\nJWT work that needs to interoperate with OAuth/OIDC, JWKS, the IANA JOSE\nalgorithm registry, or any third-party token issuer. **Use it unless you have\na specific reason not to.**\n\n`PostQuantum.AspNetCore` is a focused, deliberately *non-interoperable* tool\nfor one problem: hybrid post-quantum JWT authentication.\n\n| Concern                | `Microsoft.AspNetCore.Authentication.JwtBearer` | `PostQuantum.AspNetCore`                                  |\n|------------------------|-------------------------------------------------|-----------------------------------------------------------|\n| **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. |\n| **Quantum resistance** | None of the standard algorithms are quantum-resistant. | Hybrid: classical *and* post-quantum, both must fall.    |\n| **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. |\n| **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. |\n| **JWKS**               | First-class.                                    | `IPostQuantumJwtKeyRing` + HTTP-backed implementation — JWKS-equivalent over a deliberately trivial wire format. |\n| **External audit**     | Yes — widely deployed and reviewed.             | **No.** Preview, not audited.                              |\n| **Dependencies**       | `Microsoft.IdentityModel.*` family.             | `PostQuantum.Jwt` + the `Microsoft.AspNetCore.App` framework reference. |\n| **Target framework**   | net8 / net9 / net10.                            | `net10.0` only (matches the engine).                       |\n\n**Use `Microsoft.AspNetCore.Authentication.JwtBearer` if** you need OAuth/OIDC\ninterop, JWKS, multi-algorithm agility, or any standards-conformant JWT.\n\n**Use `PostQuantum.AspNetCore` if** you specifically want hybrid post-quantum\ntokens *now*, you control both the issuer and the verifier, and you accept\nthat your tokens won't validate in any other ecosystem until IANA registers\nthese identifiers and standard libraries catch up.\n\n### Not to be confused with…\n\n| Package                                      | What it is                                                                 | Why it isn't this  |\n|----------------------------------------------|----------------------------------------------------------------------------|--------------------|\n| **`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. |\n| **`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. |\n| **`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. |\n| **`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. |\n| **`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. |\n\n---\n\n## Migrating from `AddJwtBearer`\n\nMoving from `Microsoft.AspNetCore.Authentication.JwtBearer` is\ndeliberately a one-line change at the call site. The validation\n**model** is different (we use a static key or a custom JWKS-equivalent\nring, not an OIDC `Authority`), but the **shape** is identical: same\n`AuthenticationBuilder`, same scheme name pattern, same\n`[Authorize]` attribute, same `ClaimsPrincipal` downstream. Most\ncontrollers, policies, and middleware need no change at all.\n\n### Side-by-side\n\n**Before** — classical `JwtBearer` against an OIDC provider:\n\n```csharp\nusing Microsoft.AspNetCore.Authentication.JwtBearer;\n\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =\u003e\n    {\n        // JwtBearer discovers signing keys from the OIDC metadata document.\n        options.Authority = \"https://auth.example/\";\n        options.Audience  = \"https://api.example/\";\n        options.TokenValidationParameters.ValidIssuer = \"https://auth.example/\";\n    });\n\nbuilder.Services.AddAuthorization();\n```\n\n**After** — post-quantum `AddPostQuantumJwtBearer` against your own issuer:\n\n```csharp\nusing PostQuantum.AspNetCore;\nusing PostQuantum.Jwt;\n\nbuilder.Services\n    .AddAuthentication(PostQuantumJwtBearerDefaults.AuthenticationScheme)\n    .AddPostQuantumJwtBearer(options =\u003e\n    {\n        // No OIDC discovery — supply the ML-DSA-65 verification key directly\n        // (or via the JWKS-equivalent IPostQuantumJwtKeyRing for rotation).\n        options.ValidationParameters = new PqJwtValidationParameters\n        {\n            SignatureVerificationKey = verificationKey,\n            ValidIssuer   = \"https://issuer.example\",\n            ValidAudience = \"https://api.example\",\n        };\n    });\n\nbuilder.Services.AddAuthorization();\n```\n\nEverything downstream of those lines — `[Authorize]`,\n`[Authorize(Roles = \"...\")]`, policies, `User.FindFirst(\"sub\")`,\n`HttpContext.User.IsAuthenticated` — works **unchanged**.\n\n### What's different\n\n| Concern                  | `AddJwtBearer`                                  | `AddPostQuantumJwtBearer`                                  |\n|--------------------------|-------------------------------------------------|------------------------------------------------------------|\n| Algorithms accepted      | Full IANA catalogue (RS/PS/ES/EdDSA/HS).        | Exactly one suite: `ML-DSA-65`.                            |\n| Key source               | `Authority` (OIDC discovery) or `IssuerSigningKey`. | `SignatureVerificationKey` (static) or `IPostQuantumJwtKeyRing` (dynamic). |\n| Identity provider integration | Auth0, IdentityServer, Microsoft Entra, etc. | You issue tokens via `PqJwtBuilder`. Not OIDC-compatible.   |\n| Token size               | ~200 bytes (HMAC) → ~1 KB (RSA).                | **~4.5 KB** (ML-DSA-65 signature is 3,309 bytes).          |\n| Algorithm agility        | Yes (and historically a source of CVEs).        | **No, by design.** Token's `alg` doesn't pick a code path. |\n| Replay protection        | Not built-in.                                   | Built-in via `IPqJwtReplayCache` + Redis companion (opt-in). |\n| Standards interop        | Tokens validate in any JWT library.             | Tokens are non-interoperable until IANA registers `ML-DSA-65`. |\n| Production maturity      | Yes — decade-hardened.                          | **Preview** — not audited, not for production.             |\n\n### Run both during migration\n\nYou don't have to flip a switch — register both schemes and route\nspecific endpoints to each:\n\n```csharp\nbuilder.Services\n    .AddAuthentication()\n    .AddJwtBearer(\"Classical\", o =\u003e { o.Authority = \"https://auth.example/\"; })\n    .AddPostQuantumJwtBearer(\"PostQuantum\", o =\u003e\n    {\n        o.ValidationParameters = new PqJwtValidationParameters { /* ... */ };\n    });\n```\n\n```csharp\n[Authorize(AuthenticationSchemes = \"PostQuantum\")]\npublic class PostQuantumOnlyController : ControllerBase { }\n\n[Authorize(AuthenticationSchemes = \"Classical,PostQuantum\")]\npublic class EitherWorksController : ControllerBase { }\n```\n\nSee [`docs/RECIPES.md` § 7](docs/RECIPES.md#7-coexist-with-the-standard-jwtbearer-scheme-during-migration)\nfor the full coexistence pattern.\n\n---\n\n## Migrating from `PostQuantum.Jwt.AspNetCore`\n\nIf you're on the legacy `PostQuantum.Jwt.AspNetCore` companion\npackage (which shipped from the engine repository), `PostQuantum.AspNetCore`\nis its **renamed, repackaged successor**. Same engine, cleaner\nnaming, its own release cadence. The mapping is mechanical\n(`AddPqJwtBearer` → `AddPostQuantumJwtBearer`, `PqJwtBearer*` →\n`PostQuantumJwtBearer*`, `IPqJwtKeyRing` → `IPostQuantumJwtKeyRing`),\nand tokens minted by either package validate in the other.\n\nSee [`docs/MIGRATION.md`](docs/MIGRATION.md) for the diff-style guide.\n\n---\n\n## Security posture\n\nThe short version, honestly. **The full security contract — what the\nlibrary protects, what it does NOT protect, the replay-protection\ndeployment matrix, the key-rotation cadence, and the fail-closed\ncontract enumerated as a logical conjunction — lives in\n[`docs/SECURITY-MODEL.md`](docs/SECURITY-MODEL.md). Read that before\ndepending on this for anything that matters.**\n\n**What you get**\n\n- **Fail-closed validation.** Bad signature, tampered ciphertext, expired\n  or not-yet-valid token, wrong issuer/audience, missing `exp`, missing\n  `alg`, or an `alg` we don't expect — every one of those throws inside\n  `PqJwtValidator`, and the handler turns it into\n  `AuthenticateResult.Fail`. There is no `alg: none`, no unsigned path,\n  no silent downgrade, and no exception class from `Validate()` escapes\n  as a `500`.\n- **Native post-quantum primitives.** ML-DSA-65 and ML-KEM-768 are the\n  FIPS-validated .NET 10 BCL implementations, not a re-implementation.\n- **Hybrid by construction (for encrypted tokens).** Confidentiality stays\n  secure unless *both* X25519 and ML-KEM-768 fall.\n- **Strict, small-surface defaults.** Expiration is required, clock skew\n  is a modest 60 seconds, only the exact post-quantum algorithms are\n  accepted, the bearer prefix is matched case-insensitively per RFC 6750,\n  and the `WWW-Authenticate` realm is RFC 7235 quoted-string escaped.\n\n**What you must know**\n\n- **Not audited.** No third party has reviewed the design or\n  implementation.\n- **Non-standard identifiers.** `alg`/`enc` values (`ML-DSA-65`,\n  `X-Wing`) are not IANA-registered. Tokens are intentionally not\n  interoperable with generic JWT tooling.\n- **Preview.** Treat the API and wire format as unstable until 1.0.\n- **⚠️ Replay defence is opt-in.** **Without a configured\n  `IPqJwtReplayCache`, captured tokens are reusable until they expire.**\n  For multi-instance production, use\n  [`PostQuantum.AspNetCore.RedisReplayCache`](src/PostQuantum.AspNetCore.RedisReplayCache)\n  — see the [headline section](#distributed-replay-protection-with-redis--recommended-for-production)\n  above.\n\nFull detail in [`docs/SECURITY-MODEL.md`](docs/SECURITY-MODEL.md),\n[`SECURITY.md`](SECURITY.md), and [`KNOWN-GAPS.md`](KNOWN-GAPS.md).\n\n---\n\n## Compatibility\n\n| Surface          | Supported                                                                                  |\n|------------------|--------------------------------------------------------------------------------------------|\n| Target framework | `net10.0`                                                                                  |\n| ASP.NET Core     | 10.x via `\u003cFrameworkReference Include=\"Microsoft.AspNetCore.App\" /\u003e`                       |\n| Languages        | C# 13                                                                                      |\n| 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**. |\n| AOT / trimming   | `IsAotCompatible=true`, `IsTrimmable=true`. The HTTP key-ring JSON path is source-generated. |\n\n---\n\n## Building from source\n\n```bash\ndotnet build         # zero warnings (compiler warnings are errors)\ndotnet test          # 31 tests, zero skips on PQ-capable hosts\ndotnet format        # apply the .editorconfig style\ndotnet run --project samples/PostQuantum.AspNetCore.Demo\n```\n\nThe test suite is `Microsoft.AspNetCore.Mvc.Testing`-backed and exercises\nthe fail-closed contract end-to-end: valid token → `200 OK` with the\nexpected `ClaimsPrincipal`, every tampered/expired/wrong-issuer/wrong-audience\ncase → `401 Unauthorized`, plus assertions on each of the three event hooks.\n**Tests that exercise the native ML-DSA primitives skip themselves\nwith a stated reason** (`PqcFactAttribute`) on hosts where the BCL\nprimitives aren't available; both CI lanes (Windows native + Linux with\nOpenSSL 3.5+ via conda-forge) fail the run if any test reports skipped.\n\n---\n\n## Contributing\n\nIssues and pull requests are welcome. Before opening a PR:\n\n1. Run `dotnet build` and `dotnet test` — both must be green, with **zero\n   warnings** (the build treats compiler warnings as errors).\n2. Keep the discipline in [`CLAUDE.md`](CLAUDE.md): honesty over polish,\n   fail-closed always, no rolled-your-own crypto, native BCL first.\n3. Security-sensitive changes should land alongside a test that locks in the\n   fail-closed behaviour.\n\n**Reporting a vulnerability:** please **do not** open a public issue. Use\nGitHub's *Report a vulnerability* button on the repository, or follow the\nprocess in [`SECURITY.md`](SECURITY.md).\n\n---\n\n## License\n\n[MIT](LICENSE).\n\n---\n\n## About this library\n\nThis library was created by a human developer working in close\ncollaboration with Claude, Gemini, Grok, and ChatGPT. The vision,\ndirection, architecture decisions, and final curation were mine. The\ngoal was simple: build something the .NET ecosystem genuinely needs\nfor the post-quantum era.\n\n---\n\n*To God be the glory — 1 Corinthians 10:31.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fpostquantum-aspnetcore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsystemslibrarian%2Fpostquantum-aspnetcore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fpostquantum-aspnetcore/lists"}