⚠️ Breaking Changes
Upgrading from 2.2 needs action in a few areas — see Breaking Changes in the README for migration steps.
- Implicit Flow is opt-in: implicit and hybrid response types are rejected at client registration until the host enables them explicitly.
- Dynamic Client Registration requires an Initial Access Token by default; anonymous registration is rejected unless the requirement is turned off.
- Dynamic Client Registration rejects a back-channel logout endpoint that violates the outbound-request security policy (non-secure scheme, internal host, or private address) under the secure defaults.
- Authorization success and error responses now flow through a single formatting interface; hosts that customized error-response formatting must re-point to it.
🚀 Features
- Rich Authorization Requests (RFC 9396) (#135)
- Token Exchange (RFC 8693) (#135)
- JWT Secured Authorization Response Mode, JARM (specification) (#172)
- JWT Response for OAuth Token Introspection (RFC 9701) (#177)
- Certificate-bound access token verification (RFC 8705 §3) (#159)
- JWE-encrypted request objects (RFC 9101 §6.1) (#171)
- Signed authorization server metadata (RFC 8414 §2.1) (#127)
- Client key set binding from configuration (#115)
- DPoP sender-constrained tokens (RFC 9449) (#100, #112)
- Token type header pinning (RFC 8725 §3.11) (#80, #93)
- JOSE critical header parameter handling (RFC 7515 §4.1.11) (#75, #88, #134)
- Implicit Flow opt-in (#90, #91)
- Initial Access Token for Dynamic Client Registration (RFC 7591 §3)
- Dynamic Client Registration scope, software identifier, and software statement metadata (RFC 7591)
- Structured logging via a source generator
✏️ Improvements
- Unified client-addressed JWT signing and encryption path (#177)
- JOSE critical-header extension point reshaped to keyed-name dependency injection (#146, #148, #151)
- Host pre-registrations win the dependency-injection resolution race
- Thread-safe client-information storage
- JWT validation returns errors instead of throwing on unsupported algorithm or key combinations
- Base64URL handling migrated to the runtime primitive (#79, #89)
- Flow-type validation simplified with a filter chain (#92)
- Server-side gRPC stack removed in favour of the Protocol Buffers runtime (#68)
- Granular JWS key-resolution diagnostics (RFC 7515 §4.1.4 / §6) (#84, #97)
- Typed accessors for JWS header parameters (RFC 7515 §4.1.2 to §4.1.8) (#81, #94)
- JWS verification key pinned to its declared algorithm (RFC 7517 §4.4) (#78, #87)
- HMAC key length enforced (RFC 7518 §3.2) (#77, #86)
- Pairwise subject identifier derivation hardened to HMAC
- Authorization-response issuer parameter advertised in discovery (RFC 9207)
- Test suite parallelized via assembly fixtures and an RFC-concern split (#157)
- Migration to xunit v3 on Microsoft Testing Platform (#63)
- Testing-Platform-native test-report upload (#95)
- Central Package Management with transitive pinning (#95)
- Migration to the XML solution format with local Sonar analysis
🛠 Fixes
- Device Authorization Grant hardening (RFC 8628) (#165)
- CIBA hardening: single-use authentication request identifier, push-delivery integrity, HTTPS notification endpoint (#166, #158)
- Client authentication: constant-time secret comparison and a required unique identifier on client assertions (#163)
- Enforce the client scope set, require an expiration on JWT-bearer assertions, validate resource configuration (#167)
- Preserve allowed scopes and software identifier and version on client update (#168)
- Enforce client metadata: default maximum authentication age, default authentication context, token-endpoint authentication algorithm, UserInfo encryption (#169)
- Pin the request-object and backchannel-authentication request signing algorithms to the registered values (#170)
- Validate the encrypted-response algorithm and encryption metadata in Dynamic Client Registration (#174)
- Token Exchange identity-token origin bound through audience and authorized party (#175)
- Omit the scope from the authorization callback on pure code flow (#124)
- Pushed Authorization Requests: request reference not consumed at fetch time, JSON error instead of a redirect to login (#123, #119)
- Graceful JWT validation errors for a missing key resolver and an empty pre-bound key set (#120, #121, #122)
- CORS enabled on discovery endpoints
- Protected-resource error responses aligned with RFC 6750 and RFC 9110
- Dynamic Client Registration key-set and software-statement validation hardened
- Flaky CI failures from parallelized suites (#161)
- Pushed Authorization Requests: the request reference is now strictly single-use — consumed once an authorization code or token is issued, so it cannot be replayed within its time-to-live (RFC 9126 §6)
- Back-channel logout notifications are now reliably delivered and awaited, with per-client failure isolation so an unreachable client endpoint cannot fail the end-user's logout
- Implicit Flow response-builder dependency lifetimes corrected so the feature's components resolve consistently
Detailed Description
Rich Authorization Requests (RFC 9396)
- The authorization and token endpoints now accept fine-grained, transaction-level authorization details per RFC 9396, so a client can request specific permissions instead of coarse scopes. Each authorization detail is validated by a handler selected on its type, consented through a host-provided descriptor, and carried end-to-end into the access token and, optionally, the identity token. The flow is wired through the authorization endpoint, Pushed Authorization Requests, the token endpoint, CIBA, and the device authorization grant. The supported detail types are advertised in discovery.
Token Exchange (RFC 8693)
- A new token-exchange grant implements RFC 8693 for both impersonation and delegation. The handler resolves several subject- and actor-token formats, builds the delegation chain, and enforces a per-client allow-list of subject-token types. Discovery and Dynamic Client Registration expose the grant and its allowed token types.
JWT Secured Authorization Response Mode, JARM
- The authorization response can now be returned as a signed, and optionally encrypted, JWT per the JARM specification, selected through the JWT-based response modes. Response parameters are packed into a JWT addressed to the client, signed with the server key and encrypted to the client's registered key when configured. This defends the authorization response against tampering, mix-up, and parameter-injection attacks. Client metadata for the signing and encryption algorithms, Dynamic Client Registration binding, and discovery advertisement complete the surface.
JWT Response for OAuth Token Introspection (RFC 9701)
- When a client is registered to receive signed introspection responses and requests the JWT form through content negotiation, the introspection endpoint returns the result as a signed, and optionally encrypted, JWT per RFC 9701 instead of plain JSON. The token-state object is carried as a dedicated claim, and the JWT carries the standard issuer, audience, and issued-at claims together with the introspection media type. Content negotiation falls back to plain JSON for clients that do not request the JWT form. The three client-addressed JWT paths (UserInfo, JARM, and introspection) were unified onto a single formatting primitive that takes the encryption policy as a call parameter, removing duplicated signing-and-encryption logic. Client metadata, Dynamic Client Registration validation, and discovery of the supported signing and encryption algorithms complete the surface.
Certificate-bound access token verification (RFC 8705 §3)
- Resource-server validation now verifies that a presented access token's certificate confirmation matches the client certificate on the TLS connection per RFC 8705 §3, and the capability is advertised in discovery. A stolen mTLS-bound token is inert without the matching client certificate. This complements the issuance-side binding already present and runs alongside the DPoP proof-of-possession path.
JWE-encrypted request objects (RFC 9101 §6.1)
- The authorization request object can now arrive encrypted per RFC 9101 §6.1: the server decrypts it with its own key, then verifies the inner signature. This lets clients keep request parameters confidential in the front channel and when the request is fetched by reference.
Signed authorization server metadata (RFC 8414 §2.1)
- Discovery can now publish a signed metadata document alongside the plain one per RFC 8414 §2.1, as an opt-in feature. Relying parties that require integrity-protected discovery can verify the document was issued by the authorization server. The plain document is unchanged for clients that do not consume the signed form.
Client key set binding from configuration
- A client's public keys can now be bound directly from configuration, so they can be supplied inline at startup instead of only through a key-set URL. The binder preserves derived client-information types, and an empty pre-bound key set is treated as needing binding under the .NET 10 configuration binder.
DPoP sender-constrained tokens (RFC 9449)
- The server previously offered only the RFC 8705 mTLS path for sender-constrained tokens, which public clients (single-page, mobile, native) cannot use because they have no client TLS certificate. DPoP fills that gap with a signature-based proof channel. On the token endpoint the client signs a proof with its private key; the server validates the proof's structure and signature, computes the thumbprint of the embedded public key, and binds the issued access token to it through the confirmation claim, the same confirmation claim mTLS already populates with its certificate thumbprint, so the two proof-of-possession families coexist without a parallel claim model. The key thumbprint requested at the authorization endpoint or pushed through a Pushed Authorization Request is threaded through the grant so the binding is fixed before the token is minted. A replay cache plus a server-issued nonce (challenged through a dedicated error and a response header) block proof replay. Refresh-token binding follows the spec's split: confidential clients keep their existing client-authentication-based protection, public clients have the refresh token bound to the proof key. Resource-server validation on UserInfo confirms the presented access token's binding against a fresh proof and answers failures with the appropriate challenge. Discovery advertises the supported proof signing algorithms; Dynamic Client Registration accepts the proof-binding client metadata; host toggles gate the whole feature.
Token type header pinning (RFC 8725 §3.11)
- A new opt-in validation option pins the JWT type header so a token signed for one class (identity token, logout token, request object, DPoP proof, access token, JARM response) cannot be replayed as another by relying parties that trust the same issuer for several classes. Comparison follows RFC 7515 §5.3 (case-sensitive) with the §4.1.9 media-type-prefix-stripping convention applied before lookup. The option is off by default, preserving historical behaviour for callers that have not opted in.
JOSE critical header parameter handling (RFC 7515 §4.1.11)
- The validator now rejects a signed token whose critical-parameter list names any parameter no registered handler understands. The library ships with no handlers registered, so every well-formed critical list falls through to an unknown-critical-header-parameter rejection until a host opts in. The malformation rejections (empty list, duplicate names, standard header names, dangling references) fire on earlier guards and stay independent of the handler registry. Hosts extend support through a handler-registration extension point: each handler declares the critical parameter it understands and applies that parameter's recipient-side semantics, with the two bound inseparably, so a name cannot be advertised without the handling logic behind it. This closes the failure mode where a registration could claim a name the verification path did not actually honour, masking spec violations as accepted tokens. Signature-affecting parameters such as the RFC 7797 unencoded-payload option fall outside this post-signature contract and are deferred to a future pre-signature hook.
Implicit Flow opt-in
- New deployments default to Authorization Code Flow only: the client registration validator rejects implicit response types until the host explicitly enables Implicit Flow on the builder. This closes the registration-time gap where a client could register an implicit response type and only fail at the authorization endpoint on its first request.
Initial Access Token for Dynamic Client Registration (RFC 7591 §3)
- Anonymous Dynamic Client Registration is now rejected by default. A bearer Initial Access Token is validated for signature, expiration, type, and revocation, and a tenant-scoped revocation source lets a host revoke an individual token per RFC 7591 §3.
Dynamic Client Registration scope, software identifier, and software statement metadata (RFC 7591)
- Client registration metadata gained the requested scope set, the software identifier, and the software statement per RFC 7591. The software statement is validated as a signed JWT against a configurable trust source before its claims are applied to the registration.
Structured logging via a source generator
- All non-test code in the server now emits log lines through partial methods produced by a logging source generator: zero reflection at the call site, strongly-typed parameters, and ahead-of-time-compilation friendly. Each event carries a stable numeric identifier chosen from a documented per-feature range, so operators and audit pipelines filter by integer event-type rather than matching message text. Companion log-definition files sit next to each source file so message edits show up in their own diff.
Granular JWS key-resolution diagnostics (RFC 7515 §4.1.4 / §6)
- JWS validation previously collapsed two distinct key-resolution failures into one generic "no signing keys found" description: the issuer returned zero keys (a configuration problem) versus the issuer returned keys but none survived the algorithm and key-identifier filters (a stale-cache key-rotation incident). Audit logs need to tell them apart so a key-rotation incident is not mistaken for an issuer outage. The validator now reads the issuer key set once before filtering, branches the empty-issuer case off before the filter and the filtered-empty case off after, surfacing distinct error descriptions and emitting a structured log event with an explicit category so aggregators filter on the category, not on prose. The public error enumeration is unchanged.
Typed accessors for JWS header parameters (RFC 7515 §4.1.2 to §4.1.8)
- The JWS header model previously exposed only the algorithm, type, key-identifier, encryption, and critical-parameter fields. The six optional header parameters that carry key-locator or key-material data, the key-set URL, the embedded key, the certificate URL, the certificate chain, and the two certificate thumbprints, are now exposed as strongly-typed properties. The older SHA-1 thumbprint carries a documentation warning per RFC 7515 §10.11. The accessors are a typed surface only; consumption for key resolution is deferred to a future trust-policy hook so DPoP, federation, and client-assertion-with-embedded-key flows can land their policy without API churn here.
JWS verification key pinned to its declared algorithm (RFC 7517 §4.4)
- A key that declares an algorithm must not be used with any other algorithm. The signature verifier now filters such keys out before reaching crypto, closing within-family algorithm confusion (a key registered for one RSA variant cannot be misused to verify another) alongside the cross-family protection the keyed dispatch already provides.
Pairwise subject identifier derivation hardened to HMAC
- A pairwise subject-identifier provider and its builder extension implement the OpenID Connect pairwise-identifier algorithm with HMAC-based derivation, a configurable hash algorithm, and a base64-decoded salt. This replaces the previous string-concatenation scheme with a constant-time, key-rotatable derivation suitable for production segmentation requirements.
Authorization-response issuer parameter advertised in discovery (RFC 9207)
- Discovery now advertises that the authorization server returns its issuer identifier on every authorization response per RFC 9207. A client that checks this identifier can detect an authorization-server mix-up attack, where a response is replayed from a different server than the one the request went to. Advertising the capability lets a conforming client require and verify it.
✅ Why It Matters
- Rich Authorization Requests enable fine-grained, transaction-level consent required by Open Banking and FAPI 2.0 profiles
- Token Exchange enables delegation and impersonation for service-to-service and on-behalf-of flows
- JARM defends the authorization response against tampering, mix-up, and parameter injection by signing and optionally encrypting it
- JWT introspection responses provide resource servers with signed, verifiable token state for high-assurance deployments
- Certificate-bound access token verification ensures a stolen mTLS-bound token is inert without the matching client certificate
- CIBA, device-grant, client-authentication, and Pushed-Authorization hardening eliminates concrete replay, SSRF, timing, and polling-abuse vectors
- DPoP closes the bearer-token theft window for clients that cannot use mTLS, so a leaked access or refresh token is inert without the client's private key
- Stable numeric log identifiers and structured event-type fields let audit pipelines filter and correlate JWS validation failures without parsing free-form message text
- New RFC compliance items close concrete attack vectors and improve interoperability with strict relying-party stacks
- Implicit Flow opt-in and Initial Access Token required by default remove footguns from the secure-by-default surface
- Typed accessors for the JWS header parameters of RFC 7515 are the foundation DPoP builds on, and OpenID Federation reuses them next without API churn
- Toolchain modernization compounds: pull-request failures are diagnosable from artifacts, version drift is mechanically blocked, and ahead-of-time compilation is no longer gated by reflection-based logging
What's Changed
Full Changelog: v2.2...v2.3