DcsvIo.D2.Auth.Abstractions 0.1.1

dotnet add package DcsvIo.D2.Auth.Abstractions --version 0.1.1
                    
NuGet\Install-Package DcsvIo.D2.Auth.Abstractions -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DcsvIo.D2.Auth.Abstractions" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DcsvIo.D2.Auth.Abstractions" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="DcsvIo.D2.Auth.Abstractions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DcsvIo.D2.Auth.Abstractions --version 0.1.1
                    
#r "nuget: DcsvIo.D2.Auth.Abstractions, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DcsvIo.D2.Auth.Abstractions@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DcsvIo.D2.Auth.Abstractions&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=DcsvIo.D2.Auth.Abstractions&version=0.1.1
                    
Install as a Cake Tool

DcsvIo.D2.Auth.Abstractions

Identity / authorization vocabulary AND consumer-side runtime contracts — the value types, string constants, and read-only interfaces every consumer references when reasoning about auth. Domain layers, request-context, handler/abstractions, host-supplied JWT runtime, and issuer-side code all reference this slice; impls live in host-supplied runtime libs.

The runtime piece (AddD2Auth, JWT validation, JWKS HTTP fetcher, session liveness tracker impl) is host-supplied — those impls satisfy the IJwksProvider and ISessionLivenessTracker contracts defined here. HTTP-transport binding (middleware + ProblemDetails) and gRPC-transport binding (server-side interceptor + RpcException trailers) are likewise host-supplied. Domain code never references any of the runtime libs — only this abstractions slice.

Install

dotnet add package DcsvIo.D2.Auth.Abstractions

Types (source map)

Path Contents
ActorKind.cs Enum — Service / Impersonation. The two kinds an entry in the RFC 8693 act chain can be. Token "kind" is derived from the act chain's shape (no act → end-user; Service entry → delegation; Impersonation entry → impersonation).
ImpersonationKind.cs Enum — Consent / Force. Sub-discriminator for ActorKind.Impersonation actors: Consent = OTP-authorized (staff+admin); Force = silent (admin-only).
OrgType.cs Enum — Admin / Support / Customer / ThirdParty / Affiliate.
Role.cs Enum — Auditor / Agent / Officer / Owner. Discrete capability sets — not a hierarchy.
ActionSensitivity.cs Enum — Routine / Sensitive / Critical. Per-scope discriminator driving audit verbosity, OTP step-up triggers, and impersonation defaults.
RequestOrigin.cs Enum — Unestablished / EdgeInbound / CrossProcessHop / InProcessModule / System. What KIND of trust boundary produced the current request hop's context. Recomputed locally by the receiving boundary from its own unforgeable transport facts every hop; never propagated. Unestablished (the zero member) is the fail-closed default — a host capability authority that consumes RequestOrigin denies when the origin has not been positively established. See package docs for RequestOrigin / establishment helpers.
CallPathKind.cs Enum — Edge / WorkloadHop / ModuleHop / System. The kind of hop a CallPathEntry records.
CallPathEntry.cs sealed record CallPathEntry(string Id, CallPathKind Kind, DateTimeOffset Timestamp). One hop in the propagated call-path — the identity of the workload/module that handled the request, the hop kind, and when it was reached. Operational telemetry only; authority code never reads the call-path.
D2WorkloadIdentityOptions.cs Options class — ServiceId (the host's own workload identity, the same value its mTLS leaf SAN encodes). Read by every establishment boundary (Edge-inbound HTTP middleware, cross-process gRPC interceptor, System-worker bootstrap) so each appends the SAME self-id to the call-path.
ActorEntry.cs sealed record ActorEntry(ActorKind Kind, string Subject, string? ClientId, ImpersonationKind?, Guid? SessionId, Guid? OrgId, string? OrgName, OrgType?, Role? OrgRole, ActorEntry? Act). The recursive Act field models RFC 8693 §2.1 nested chains; the ImpersonationKind / SessionId / four Org* fields apply when Kind == Impersonation (they describe the agent / impersonator's own context).
Scopes.g.cs (codegen, in Generated/DcsvIo.D2.Auth.Scopes.SourceGen/...) Static partial class — OAuth-canonical scope string constants emitted from contracts/auth-scopes/scopes.spec.json by the sibling DcsvIo.D2.Auth.Scopes.SourceGen analyzer. Single source of truth for the platform's scope catalog.
Audiences.g.cs (codegen, in Generated/DcsvIo.D2.Auth.Audiences.SourceGen/...) Static partial class — JWT aud-claim audience constants emitted from contracts/auth-audiences/audiences.spec.json by the sibling DcsvIo.D2.Auth.Audiences.SourceGen analyzer. Single source of truth for the inbound aud-claim check at every hop — under the forward-unchanged model that check is the broad internal audience every internal service accepts — and for the targetAudience argument of the retained boundary-mint / exception token exchanges (TokenExchangeClient.ExchangeAsync). Provides IsKnown(url), Resolve(name), ResolveByUrl(url) helpers, plus AllUrls (read-only set of every audience URL) and ByName (read-only name → URL map) collection projections for enumeration.
WellKnownAudiences.cs Static class (hand-declared, NOT codegen) — well-known JWT aud audiences deliberately absent from audiences.spec.json. Holds D2_INTERNAL_AUDIENCE ("d2.internal"), the single broad internal receive audience every hop validates and the Edge minter sets under the forward-unchanged model. Hand-declared because it is a protocol constant, not a token-exchange target like the spec-generated Audiences entries — so it is not a spec-mirror DTO.
ForwardedJwt.cs readonly struct — redacting wrapper around the raw internal transaction-token (an RS256 JWT bearer string) a hop retains to replay byte-for-byte on an outbound cross-process gRPC hop. Unloggable by construction (forward-unchanged realization): carries [RedactData(SecretInformation)], a self-redacting ToString(), the raw bytes in a PRIVATE field (invisible to the Serilog destructuring policy, which reflects only public properties), and a single reveal seam RevealForForwarding(). Create(string?) → D2Result<ForwardedJwt> validates a blank credential away (no JWT-format re-check — the token is pre-validated at the capture site).
IForwardedJwtAccessor.cs Interface — request-scoped holder for the inbound forwarded JWT (Current read + Capture(rawBearer) write). Structurally isolated from IRequestContext (a distinct type with a distinct DI registration, never projected by the request-context enricher) so the live credential cannot leak through the broadly-projected log/telemetry surface. Registered request-scoped by both AddD2AuthHttp() and AddD2AuthGrpc(); populated by the HTTP middleware / gRPC interceptor after successful validation; read by the outbound forwarding credential.
MutableForwardedJwtAccessor.cs public sealed class — default IForwardedJwtAccessor impl, a plain per-request mutable cell with no static state (request isolation = the scoped DI lifetime). Public because the DI registration sites live in the separate transport assemblies. Capture routes through ForwardedJwt.Create (blank input validated away, never stored); a second capture in one scope is last-write-wins.
IAmbientRequestScopeAccessor.cs Framework-free port — IServiceProvider? Current { get; } — that the outbound forwarding credential (ForwardedJwtCallCredentials in host-supplied outbound runtime) depends on to reach the current request's request-scoped IForwardedJwtAccessor per outbound RPC. Lives here (rather than in the outbound lib) so BOTH the outbound lib and the inbound transport libs reference the port from a shared, framework-free home — eliminating the otherwise necessary auth/http → auth/outbound package reference that would drag the entire outbound gRPC stack into every HTTP-inbound consumer. Each transport lib supplies its OWN IHttpContextAccessor-backed adapter (a deliberate tiny duplicate, since the two transport libs have no inter-csproj dep): e.g. HttpContextAmbientRequestScopeAccessor (registered by AddD2AuthHttp()) and GrpcHttpContextAmbientRequestScopeAccessor (registered by AddD2AuthGrpc()).
JwtClaimTypes.g.cs (codegen, in Generated/DcsvIo.D2.Auth.JwtClaims.SourceGen/...) Static class — claim name constants emitted from contracts/jwt-claims/jwt-claims.spec.json by the sibling DcsvIo.D2.Auth.JwtClaims.SourceGen analyzer. Standard claims (sub, aud, act, scope, ...) keep canonical names; D² custom claims use the d2_ prefix. The act.d2_kind claim discriminates impersonation flavor (Consent vs Force) — see ImpersonationKind for the values. Same spec drives the TS-side @dcsv-io/d2-auth-abstractions JwtClaimTypes catalog.
Jwks/IJwksProvider.cs Interface — GetKeysAsync / RefreshAsync returning D2Result<JwksKeySetSnapshot> / D2Result. The contract every consumer-side service uses to look up JWT verify keys by kid. Implementations are host-supplied (this package is the port only).
Jwks/JwksKeySetSnapshot.cs Sealed record — immutable snapshot of the verify-key set (kid → SecurityKey dictionary + fetched-at timestamp + source URL). Returned by IJwksProvider.GetKeysAsync.
Sessions/ISessionLivenessTracker.cs Interface — IsAliveAsync(sessionId) returning D2Result<bool>. Read-only contract every authenticated request uses to verify the session is still alive. Cache outage = ServiceUnavailable = caller fails closed (401) — never treat unknown liveness as alive. Implementations are host-supplied. Session writes and invalidation are host concerns; this package only defines the read-side port.
Http/D2HttpContextItems.g.cs (codegen, in Generated/DcsvIo.D2.InProcessKeys.SourceGen/...) Static class — string-key constants for cross-transport HttpContext.Items slots emitted from contracts/in-process-keys/keys.spec.json by the sibling DcsvIo.D2.InProcessKeys.SourceGen analyzer. Defines REQUEST_CONTEXT ("D2.RequestContext"), the slot under which the inbound auth runtime writes the populated IRequestContext on successful auth. Lives in the abstractions slice so BOTH host-supplied transport-binding projects (HTTP middleware + gRPC interceptor) can write to the same slot without an inter-csproj dep. The same spec drives the gRPC-side DcsvIo.D2.Auth.Grpc.Interceptors.D2GrpcUserStateKeys catalog at identical wire values.

The Generated/ directory is tracked in git and contains the SourceGen-emitted output — committed for inspection, IDE navigation, and PR diff review. Files are re-emitted on every dotnet build from the spec contracts; never hand-edit. The *.g.cs glob is marked linguist-generated=true in .gitattributes so GitHub PR UI collapses these diffs by default.


Public API surface

ActorKind + ImpersonationKind — RFC 8693 / RFC 6749 §4.4 token taxonomy

ActorKind.Service           // act entry is a service identity (RFC 6749 §4.4 client_credentials)
ActorKind.Impersonation     // act entry is a user impersonating another user

ImpersonationKind.Consent   // OTP-authorized impersonation (staff + admin orgs)
ImpersonationKind.Force     // silent impersonation (admin orgs only, dev/support fallback)

Token shape (derived from act chain):

Act chain shape Effective token kind
Empty (no act claim) End-user direct token (only at Edge from browser requests)
Outermost entry has Kind == Service Service-on-behalf-of-user delegation OR pure service-identity token
Outermost entry has Kind == Impersonation User impersonation; entry.ImpersonationKind says Consent or Force

OrgType / Role enums

Used as typed properties on IAuthContext (auth/context-abstractions). Wire format: lowercase string in JWT claims (d2_org_type, d2_org_role). The codegen-emitted MutableRequestContext.FromClaims(...) handles parse / format.

The JWT carries a single org context — during impersonation, the JWT's org claim is the impersonated user's org. The agent's own org is recorded inside the act chain for audit and for authz rules that key on the agent's home org.

ActorEntry record

public sealed record ActorEntry(
    ActorKind Kind,
    string Subject,                            // act.sub — user id (Impersonation) or service client_id (Service)
    string? ClientId = null,                   // when Service: OAuth client_id (often == Subject)
    ImpersonationKind? ImpersonationKind = null,  // when Impersonation: Consent or Force
    Guid? SessionId = null,                    // when Impersonation: act.d2_session_id (the impersonation session, distinct from the user's own session)
    Guid? OrgId = null,                        // when Impersonation: act.d2_org_id — the agent's own org
    string? OrgName = null,                    // when Impersonation: act.d2_org_name
    OrgType? OrgType = null,                   // when Impersonation: act.d2_org_type
    Role? OrgRole = null,                      // when Impersonation: act.d2_org_role — agent's role in their own org
    ActorEntry? Act = null);                   // RFC 8693 §2.1 nested chain

IAuthContext.ActorChain exposes this as IReadOnlyList<ActorEntry> for ergonomic enumeration. Each entry's Act field walks the nested chain when delegation-of-delegation occurs (e.g. Edge → Notifications → Files all carrying user identity).

The four Org* fields on Impersonation entries carry the agent's own organizational context — useful for audit ("Alice from Customer Support impersonated Bob") and for authz rules that key on the agent's home org. IAuthContext exposes these as derived top-level convenience properties (ImpersonatorOrgId, ImpersonatorOrgName, ImpersonatorOrgType, ImpersonatorOrgRole) — null when not impersonating.

Constant classes

JwtClaimTypes.SUB                          // "sub"
JwtClaimTypes.SCOPE                        // "scope"
JwtClaimTypes.ACT                          // "act"
JwtClaimTypes.CLIENT_ID                    // "client_id"      (RFC 8693 §4.3 / RFC 9068 §2.2)
JwtClaimTypes.SESSION_ID                   // "d2_session_id"
JwtClaimTypes.ORG_ID                       // "d2_org_id"
JwtClaimTypes.ORG_ROLE                     // "d2_org_role"
JwtClaimTypes.ACT_KIND                     // "d2_kind"        (lookup path: act.d2_kind)
JwtClaimTypes.ACT_SESSION_ID               // "d2_session_id"  (lookup path: act.d2_session_id)
// ... plus more

// Wire-protocol header constants live in the per-transport catalogs
// (DcsvIo.D2.Headers.{Common,Http,Amqp,Grpc}). See ../README.md § Libraries
// for the full per-transport enumeration.

Scopes.Self.Read                           // "self.read"   (codegen-emitted)
Scopes.Auth.Password.Change                // "auth.password.change"
// (full catalog lives in contracts/auth-scopes/scopes.spec.json)

WellKnownAudiences.D2_INTERNAL_AUDIENCE    // "d2.internal" (hand-declared, NOT spec-derived —
                                           //  the universal internal receive audience every hop validates)

RequestOrigin + CallPath — local establishment facts vs propagated telemetry

What kind of trust boundary produced the current request hop's context, and what sequence of hops the request has traversed so far, are two structurally distinct kinds of fact:

RequestOrigin.Unestablished    // no boundary has positively established this yet — fail-closed
RequestOrigin.EdgeInbound      // the external HTTP boundary; starts the call-path
RequestOrigin.CrossProcessHop  // a gRPC hop authenticated by a validated mTLS client certificate
RequestOrigin.InProcessModule  // an in-host module call through the generated I<Module>Api leaf
RequestOrigin.System           // an in-host background worker with no inbound user request

CallPathKind.Edge          // external HTTP / API-gateway inbound boundary (public enum member name)
CallPathKind.WorkloadHop   // a cross-process mTLS-authenticated hop
CallPathKind.ModuleHop     // an in-process module hop
CallPathKind.System        // an in-host system worker

IRequestContext.Origin and its paired ImmediateCaller are never propagated — every establishment boundary (the Edge-inbound HTTP middleware, the cross-process gRPC interceptor, the in-process-module boundary, the System-worker bootstrap — all in DcsvIo.D2.Context.Abstractions

  • the transport-binding libs) recomputes both fresh from its own local, unforgeable transport evidence every hop. RequestOrigin.Unestablished (the enum's zero member) is the scoped default, so a context no boundary has touched fails closed by construction — a host capability authority that consumes RequestOrigin denies rather than assume a plane.

CallPath (IReadOnlyList<CallPathEntry>) is the opposite shape on purpose: it DOES propagate on x-d2-context (depth-bounded — see CallPathOps in DcsvIo.D2.Context.Abstractions), accumulating each hop's own CallPathEntry as the request travels. It is operational telemetry only — authority code never takes a CallPath parameter.

Forwarded-JWT credential — ForwardedJwt + IForwardedJwtAccessor

The in-process realization of the forward-unchanged rule — how a hop holds the forwarded transaction-token so it can re-attach it outbound.

// Smart constructor — validates a blank credential away; NO JWT-format re-check
// (the token is already validated at the capture site).
D2Result<ForwardedJwt> result = ForwardedJwt.Create(rawBearer);

// THE sole reveal seam — the only path to the raw bytes (used by the outbound
// forwarding credential). Throws on an empty wrapper.
string raw = forwardedJwt.RevealForForwarding();

// Unloggable by construction — every log/serialization path yields the placeholder.
forwardedJwt.ToString();                   // "[REDACTED: ForwardedJwt]"
// {@x} destructuring → "[REDACTED: SecretInformation]" via the [RedactData] policy.

// Request-scoped holder — populated by the inbound auth surface, read by the
// outbound forwarding credential. Registered by AddD2AuthHttp() + AddD2AuthGrpc().
ForwardedJwt? current = accessor.Current;
accessor.Capture(rawBearer);

Four-layer never-logged guarantee (proven by tests, not asserted): self-redacting (ToString + [RedactData] + private backing field), single reveal seam (RevealForForwarding), enrichment-isolated (held off IRequestContext, so the request-context enricher cannot reach it), and never a [LoggerMessage] parameter.


Dependencies

  • DcsvIo.D2.ResultD2Result<T> returns on the consumer-side contracts (IJwksProvider, ISessionLivenessTracker) and ForwardedJwt.Create. Transitively re-exposes ErrorCategory (used by ForwardedJwt.Create's generic ValidationFailed).
  • DcsvIo.D2.I18n.AbstractionsTKMessage parameters via D2Result.Messages / InputErrors shape.
  • DcsvIo.D2.Utilities[RedactData] + RedactReason for the ForwardedJwt redacting wrapper, and ToNullIfEmpty() for its smart constructor. A leaf (references only result/i18n), so no cycle.
  • Microsoft.IdentityModel.TokensSecurityKey type used on JwksKeySetSnapshot.Keys. The de-facto BCL-adjacent abstraction (ASP.NET Core's TokenValidationParameters.IssuerSigningKeys takes it; ConfigurationManager<OpenIdConnectConfiguration> emits it). No deeper abstraction layer adds value here.
  • Analyzer-only project references to DcsvIo.D2.Auth.Scopes.SourceGen and DcsvIo.D2.Auth.Audiences.SourceGen for the codegen output.

The package carries the consumer-side runtime contract surface — interfaces + result-shaped types — but no impl logic. Consumer-side runtime libs and Edge's issuer-side code both target these contracts.


Design notes

Why d2_ prefix on custom claims

Custom JWT claims are namespaced with the d2_ prefix to avoid future spec collisions. Standard OAuth / OIDC claims (sub, aud, iat, exp, azp, scope, act) keep their canonical names because they're spec-defined.

Why act chain is recursive

RFC 8693 §2.1 defines the actor claim recursively: { "sub": "...", "act": { "sub": "...", "act": { ... } } }. Each link represents one delegation / impersonation step. Flat-list modeling would lose audit fidelity for multi-hop scenarios (e.g. Edge service forwards an impersonation token through Notifications onto Files).

Per RFC 8693 §4.1: the outermost act claim represents the current actor; the least recent actor is the most deeply nested. The ActorChainParser flattens to an IReadOnlyList<ActorEntry> ordered outermost-first, so chain[0] is the immediate caller and chain[chain.Count - 1] is the originator (the first service that started the call chain).

⚠ Hard requirement on the auth runtime: preserve nested act whenever a token IS exchanged

RFC 8693 §4.1 leaves it to the AS's discretion whether to preserve nested act history when issuing an exchanged token. Many ASes drop nesting and only carry the immediate caller forward.

This requirement is about what happens when a token exchange occurs — not about exchanging on every hop. Under the forward-unchanged service-to-service model, an ordinary internal hop forwards the edge-minted token byte-for-byte and does not exchange, so its act chain is the one set at the boundary mint and is left untouched the whole way down. The chain is established (or extended) only where an exchange genuinely happens: the single boundary mint at the edge, and the deliberate exception cases token exchange is retained for — most relevantly impersonation, which adds an actor entry. (The chain therefore does not grow link-by-link as a request descends the call graph; it is set once at the mint and forwarded.)

Whenever the host-supplied auth runtime DOES mint or exchange a token that carries an actor, it MUST preserve the nested act history. Specifically: when an exchange takes a token with act = { sub: B, act: { sub: Edge } } and adds a new actor A, the resulting token's act must be { sub: A, act: { sub: B, act: { sub: Edge } } } — A is added as the new immediate actor and the prior chain is preserved unchanged. Dropping nesting would make IAuthContext.OriginatingClientId unrecoverable beyond the most recent actor and break audit traceability across multi-actor chains.

The exchange-helper API on the auth runtime accepts an optional prior_actor_chain parameter for the callers that forward an existing chain — used only on the exchange path itself (the boundary mint and the retained exception cases), never on an ordinary forward-unchanged hop. (Async consumers do not propagate identity claims via the wire — identity rebuilds from the JWT each sync hop, and the asynchronous path carries only the encrypted operational subset.)

The depth limit lives in DcsvIo.D2.Context.Abstractions.ActorChainParser.MaxActDepth (currently 20). The auth runtime enforces the same limit at mint time so issued tokens never exceed what consumers can parse — a DoS guard on the chain structure, independent of where the chain is established.

Strict-mode parsing of the act chain

DcsvIo.D2.Context.Abstractions.ActorChainParser rejects malformed actor chains by throwing MalformedActorChainException:

  • Any entry missing sub (RFC 8693 §2.1 violation)
  • Any impersonation entry missing d2_kind / d2_session_id / d2_org_id / d2_org_type / d2_org_role
  • Depth exceeds MaxActDepth (DoS protection)
  • Invalid JSON or non-object root

Auth middleware MUST catch and convert to D2Result.Unauthorized (HTTP 401) — a malformed actor chain is a signed-token-with-bad-payload condition that should never reach a handler.

Why every constant catalog is codegen-emitted

The scope catalog, audience catalog, JWT claim names, HTTP header names, and in-process slot keys are ALL codegen-emitted from per-topic specs in contracts/. Each spec is a single source of truth that drives BOTH the .NET catalog AND the corresponding TS catalog (where applicable) — cross-language drift is structurally impossible.

  • Scopes.g.cscontracts/auth-scopes/scopes.spec.json via DcsvIo.D2.Auth.Scopes.SourceGen
  • Audiences.g.cscontracts/auth-audiences/audiences.spec.json via DcsvIo.D2.Auth.Audiences.SourceGen
  • JwtClaimTypes.g.cscontracts/jwt-claims/jwt-claims.spec.json via DcsvIo.D2.Auth.JwtClaims.SourceGen
  • Http/D2HttpContextItems.g.cscontracts/in-process-keys/keys.spec.json via DcsvIo.D2.InProcessKeys.SourceGen
  • HTTP / AMQP / gRPC / cross-transport header catalogs ← contracts/headers/headers.spec.json via DcsvIo.D2.Headers.SourceGen (live in their own per-transport csprojs DcsvIo.D2.Headers.{Http,Amqp,Grpc,Common})

Tests

packages/dotnet/tests/Unit/Auth/:

  • ActorKindTests.cs / ImpersonationKindTests.cs / OrgTypeTests.cs / RoleTests.cs / ActionSensitivityTests.cs — enum value stability (rename = breaking change gate).
  • ActorEntryTests.cs — record equality including nested Act chains; null-Act default; multi-level chain traversal; ImpersonationKind / SessionId only-meaningful-when-Impersonation discipline.
  • JwtClaimTypesTests.cs — per-VALUE pin against the codegen-emitted catalog; D²-prefix validation.
  • WellKnownAudiencesTests.cs — value pin on D2_INTERNAL_AUDIENCE; the hand-declared-not-spec-derived guard (the value is absent from every Audiences projection — the codegen boundary); adversarial no-duplicate-constant walk.
  • HTTP / AMQP / gRPC / common header per-VALUE pins live in tests/Unit/Headers/HeaderCatalogPinTests.cs.
  • SpecsConsistency/JwtClaimsVsIAuthContextConsistencyTests.cs — every claim: annotation in IAuthContext.spec.json references a valid jwt-claims spec entry (forward direction; reverse intentionally not enforced — ACT_KIND / IAT / EXP etc. live outside top-level IAuthContext properties).

packages/dotnet/tests/Unit/Auth/Jwks/:

  • JwksKeySetSnapshotTests.cs — defensive-copy guarantee on the Keys init setter, per-property record equality, empty-key-set tolerance.
  • IJwksProviderContractTests.cs — reflection-based shape pinning (return type, parameter count, ct default, public + interface, exact method count).

packages/dotnet/tests/Unit/Auth/Sessions/:

  • ISessionLivenessTrackerContractTests.cs — same reflection-based shape pinning so accidental method additions / removals fire at the abstractions test layer first.

packages/dotnet/tests/Unit/Auth/Inbound/Forwarding/ — the forwarded-JWT credential:

  • ForwardedJwtTests.csCreate (valid / null / empty / whitespace / oversized / control-chars-verbatim); RevealForForwarding (exact bytes / empty-throws / sole raw accessor / no public raw property); the never-logged proofs (ToString / interpolation / JSON-serialize / [RedactData]-attribute / real-destructuring-policy all yield the placeholder, never the bytes); equality / hashcode / HasValue.
  • MutableForwardedJwtAccessorTests.csCurrent / Capture / double-capture (last-write-wins) / blank-input-no-op / no-shared-state.
  • ForwardedJwtAccessorResolutionTests.csAddD2AuthHttp() + AddD2AuthGrpc() each register a RESOLVABLE scoped holder (resolve, not descriptor-presence); scoped-lifetime + cross-request-bleed + cross-transport parity + dual-transport.
  • ForwardedJwtLogDelegateContractTests.cs — scans every auth *Log class; asserts no [LoggerMessage] delegate takes a ForwardedJwt parameter (mirrors MtlsLogDelegateContractTests).
  • ForwardedJwtSoleRevealCallerTests.cs — source-text scan over server/; asserts no unexpected production caller of RevealForForwarding (the single controlled reveal seam).

packages/dotnet/tests/Unit/Logging/Internal/ForwardedJwtEnrichmentExclusionTests.cs — structural-isolation pin: no ForwardedJwt-typed / credential-named property on IRequestContext / IAuthContext, and the request-context enricher records no credential field/bytes even with a populated holder in scope.

packages/dotnet/tests/Integration/Logging/ForwardedJwtLogCaptureTests.cs — end-to-end Serilog log-capture across a capture-then-reveal cycle; asserts the raw bytes surface in NO event across both the {@x} and {x} paths.

Run: dotnet test packages/dotnet/tests.


Reference

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on DcsvIo.D2.Auth.Abstractions:

Package Downloads
DcsvIo.D2.AuthContext.Abstractions

Read-only IAuthContext interface for reasoning about caller identity, organization, scopes, and impersonation context in D2.

DcsvIo.D2.Context.Abstractions

Spec-driven request-context primitives for D2 — IRequestContext, MutableRequestContext, and the cross-hop propagation codecs.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 267 7/17/2026
0.1.0 266 7/17/2026