Device Bound Session Credentials (DBSC) for cookie authentication (prototype)#67388
Device Bound Session Credentials (DBSC) for cookie authentication (prototype)#67388rokonec wants to merge 30 commits into
Conversation
Implements the DBSC protocol (W3C spec) in the cookie authentication handler. Uses a stateless two-cookie pattern: - Long-lived cookie: auth ticket + embedded public key (refresh token equivalent) - Short-lived cookie (__dbsc suffix): device-bound credential (access token equivalent) Includes: - Registration/refresh middleware - ES256/RS256 JWT validation - Stateless challenge generation via DataProtection - Sample app with HTTP logging for testing - 16 unit/integration tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New separate authentication handler project following the OAuth/OIDC delegation pattern. The DBSC handler manages the registration/refresh protocol and delegates cookie stamping to separate cookie authentication schemes. Architecture: - Policy scheme routes auth: tries Session cookie first, falls back to source - Registration handler reads source scheme, stamps Refresh + Session cookies - Refresh handler reads Refresh cookie (path-scoped), stamps fresh Session cookie - Source (long-lived) cookie deleted after registration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sample app demonstrates the new DBSC architecture: - Source cookie scheme with DeviceBoundSessionCookieEvents - AddDeviceBoundSession() wires up refresh/session/policy schemes - Policy scheme fallback verified working Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The v1 approach embedded DBSC directly in the cookie authentication handler. This has been replaced by the v2 architecture in the separate DeviceBoundSessions project which uses the OAuth/OIDC delegation pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tyModel JWT - Challenges now use ITimeLimitedDataProtector (5min expiry) and include the user's NameIdentifier claim for binding validation - Registration validates NameIdentifier from challenge matches the authenticated principal - Cookie sign-in header emission is now automatic via IPostConfigureOptions (no manual DeviceBoundSessionCookieEvents wiring in sample) - JWT validation uses Microsoft.IdentityModel.JsonWebTokens - Base64Url encoding uses WebEncoders from framework - Added DataProtection.Extensions and WebUtilities references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove IsAspNetCoreApp and IsPackable=false (matches JwtBearer pattern) - Revert SharedFramework.External.props changes - Add Microsoft.IdentityModel.JsonWebTokens to Dependencies.props/Versions.props as a LatestPackageReference (same as OpenIdConnect, WsFederation) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace simple NameIdentifier with a stable user identifier that follows the same priority as antiforgery: sub > NameIdentifier > UPN > SHA256 hash of all claims. This ensures the challenge is bound to the user regardless of which identity claims are available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Registration challenge: claimUid|nonce (no session ID, not known yet) - Refresh challenge: claimUid|nonce|sessionId (binds to specific session) - Use RandomNumberGenerator.Fill(Span<byte>) instead of GetBytes() - Use stackalloc for nonce/hash buffers - Remove the RegistrationSessionId constant (was a design smell) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace pipe-separated string with CBOR map encoding
Registration: { uid: claimUid }
Refresh: { uid: claimUid, sid: sessionId }
- Remove nonce (ITimeLimitedDataProtector already provides uniqueness
and replay protection via timestamp + MAC)
- Use byte[] overload of ITimeLimitedDataProtector.Protect/Unprotect
- Add System.Formats.Cbor reference
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use CBOR definite-length array instead of map (simpler, smaller): Registration: [ claimUid ] Refresh: [ claimUid, sessionId ] - Use WebEncoders.Base64UrlEncode/Decode consistently (not manual replace) - ITimeLimitedDataProtector byte[] API is appropriate here (not hot path, ISpanDataProtector doesn't support time-limited) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Challenge payload is just consecutive CBOR text strings, no array framing: Registration: textstring(claimUid) Refresh: textstring(claimUid) textstring(sessionId) - Default ShortLivedCookieExpiration changed from 30s to 10 minutes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Switch from Microsoft.IdentityModel.JsonWebTokens (new entry) to Microsoft.IdentityModel.Protocols.OpenIdConnect (already in repo, transitively includes JsonWebTokens) - Remove added Dependencies.props and Versions.props entries - ComputeClaimUid now uses CBOR encoding for claim triples: - Known claims (sub/nid/upn): CBOR sequence of 3 text strings - Fallback: SHA256 of CBOR-encoded sorted claims (no intermediate strings) - No more string interpolation for claim UIDs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Split DeviceBoundSessionConfiguration.cs into separate files per class - Split DeviceBoundSessionCookieEvents.cs into PostConfigure, EventsWrapper, SourceSchemes - Move DeviceBoundSessionJwtResult to own file - Remove unused SessionCookieSuffix and RefreshCookieSuffix from Defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rotector - PostConfigureDeviceBoundSessionDerivedCookieOptions copies HttpOnly, Secure, SameSite, Domain, ExpireTimeSpan from source cookie scheme to refresh and session cookie schemes - Refresh cookie overrides path to /.well-known/dbsc - DeviceBoundSessionChallengeProtector is now a non-static singleton that caches the ITimeLimitedDataProtector instance - Handler creates protector from injected IDataProtectionProvider - Removed TryAddSingleton for protector (handler creates its own) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reflects: NuGet package model, CBOR challenges without nonce, ITimeLimitedDataProtector, antiforgery-style claim UID, cookie settings inheritance via IPostConfigureOptions, 10min default expiry, file structure, JWT validation via Microsoft.IdentityModel, singleton challenge protector. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pand debug sample - emit Secure-Session-Challenge only on 403, not on success - rename JSON DTOs to SessionInstruction/Scope/ScopeRule/Credential - add DbscSampleV2 debug dashboard and HAR capture - add challenge protector and JWT validator tests 🔒 - Generated by Copilot
…ntal - add per-reason debug logging in challenge protector and JWT validator - separate DP purposes for registration/refresh; fix CBOR validation - gate public API as experimental (ASP0030); anchor refresh cookie lifetime - rename DbscSampleV2 sample to DbscDebugServer; add gap tests 🔒 - Generated by Copilot
- inherit SlidingExpiration from the source scheme; drop refresh-scheme override - stop anchoring refresh cookie lifetime at registration so it slides like the auth cookie - add tests for refresh sliding inheritance and short-lived cookie staying fixed - sample: surface the refresh cookie on the dashboard and decode challenges with split purposes 🔒 - Generated by Copilot
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: rokonec <25249058+rokonec@users.noreply.github.com>
Co-authored-by: rokonec <25249058+rokonec@users.noreply.github.com>
- document experimental ASP0030 gating and per-reason logging - describe domain-separated challenge protectors and refresh-cookie sliding - refresh file structure and add the lifetime-anchoring alternative 🔒 - Generated by Copilot
There was a problem hiding this comment.
Pull request overview
Introduces a new experimental authentication component (Microsoft.AspNetCore.Authentication.DeviceBoundSessions) that implements the server side of the W3C Device Bound Session Credentials (DBSC) draft for hardening cookie authentication via device-bound proof-of-possession refresh.
Changes:
- Adds a new
Microsoft.AspNetCore.Authentication.DeviceBoundSessionspackage with handler/options/extensions, JWT proof validation, and challenge protection. - Adds unit tests for challenge protection, JWT validation, and derived-cookie protection/sliding behavior.
- Adds a
DbscDebugServersample plus repo wiring (project references, solution entries, diagnostic list update).
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj | References the new DeviceBoundSessions project from the shared authentication test project. |
| src/Security/Authentication/test/DeviceBoundSessions/DeviceBoundSessionJwtValidatorTests.cs | Adds tests covering JWT proof validation behaviors and failure modes. |
| src/Security/Authentication/test/DeviceBoundSessions/DeviceBoundSessionCookieProtectionTests.cs | Adds tests verifying derived cookie schemes remain data-protected and inherit sliding/lifetime behavior. |
| src/Security/Authentication/test/DeviceBoundSessions/DeviceBoundSessionChallengeProtectorTests.cs | Adds tests for registration/refresh challenge round-trips and domain separation. |
| src/Security/Authentication/test/DeviceBoundSessions/DbscProofKey.cs | Test helper for minting ES256/RS256 DBSC proof JWTs. |
| src/Security/Authentication/DeviceBoundSessions/src/SessionScopeRule.cs | JSON DTO for session scope rules (wire format). |
| src/Security/Authentication/DeviceBoundSessions/src/SessionScope.cs | JSON DTO for session scope (wire format). |
| src/Security/Authentication/DeviceBoundSessions/src/SessionInstruction.cs | JSON DTO for the session instruction response. |
| src/Security/Authentication/DeviceBoundSessions/src/SessionCredential.cs | JSON DTO for credentials list entries (cookie credential). |
| src/Security/Authentication/DeviceBoundSessions/src/PublicAPI.Unshipped.txt | Declares new public API surface (experimental-gated). |
| src/Security/Authentication/DeviceBoundSessions/src/PublicAPI.Shipped.txt | Initializes shipped API file for the new project. |
| src/Security/Authentication/DeviceBoundSessions/src/PostConfigureDeviceBoundSessionDerivedCookieOptions.cs | Copies cookie settings from source scheme to refresh/session derived schemes. |
| src/Security/Authentication/DeviceBoundSessions/src/PostConfigureDeviceBoundSessionCookieOptions.cs | Hooks Secure-Session-Registration header emission into cookie sign-in. |
| src/Security/Authentication/DeviceBoundSessions/src/Microsoft.AspNetCore.Authentication.DeviceBoundSessions.csproj | Defines the new package project and its references/InternalsVisibleTo. |
| src/Security/Authentication/DeviceBoundSessions/src/LoggingExtensions.cs | Adds source-generated logging events for DBSC validation failures. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionSourceSchemes.cs | Tracks source/derived scheme mappings for post-configure logic. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionScopeRule.cs | Options model for configuring scope rules. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionRegistrationHeader.cs | Emits the Secure-Session-Registration response header. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionOptions.cs | Options for DBSC handler (paths, schemes, lifetime, scope, allowed initiators). |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionJwtValidator.cs | Validates DBSC proof JWTs (ES256/RS256) and extracts claims. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionJwtResult.cs | Result model for validated proof JWTs. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionJsonContext.cs | Source-generated JSON serialization context for DBSC DTOs. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionHttpContextExtensions.cs | Public HttpContext extension to emit registration header for already-signed-in users. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionHandler.cs | Main protocol handler for registration/refresh endpoints and cookie minting. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionExtensions.cs | AddDeviceBoundSession(...) registration wiring (policy + derived cookie schemes + handler). |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionDefaults.cs | Default scheme and endpoint paths. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionCookieEvents.cs | Events decorator for EventsType cookie scenarios to emit registration header. |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionConstants.cs | Protocol constants (headers, algorithms, advertised list). |
| src/Security/Authentication/DeviceBoundSessions/src/DeviceBoundSessionChallengeProtector.cs | Time-limited DP + CBOR challenge minting/validation with domain separation. |
| src/Security/Authentication/DeviceBoundSessions/samples/DbscDebugServer/Properties/launchSettings.json | Launch profile for the debug server sample. |
| src/Security/Authentication/DeviceBoundSessions/samples/DbscDebugServer/Program.cs | Debug server sample host and endpoints for manual protocol exercise. |
| src/Security/Authentication/DeviceBoundSessions/samples/DbscDebugServer/HarLoggingMiddleware.cs | Sample middleware to log traffic into HAR with DBSC annotations. |
| src/Security/Authentication/DeviceBoundSessions/samples/DbscDebugServer/DbscDebugServer.csproj | Sample project file referencing the new DBSC package and dependencies. |
| src/Security/Authentication/DeviceBoundSessions/samples/DbscDebugServer/DbscDebug.cs | Sample debug state, decoders, and capture middleware for the dashboard. |
| src/Security/Authentication/DeviceBoundSessions/samples/DbscDebugServer/Dashboard.cs | Embedded HTML/JS dashboard UI for live protocol inspection. |
| src/Security/Authentication/DeviceBoundSessions/plans/dbsc-design.md | Design document describing goals, scheme topology, protocol flow, and risks. |
| src/Security/Authentication/Cookies/src/Microsoft.AspNetCore.Authentication.Cookies.csproj | Adds InternalsVisibleTo for authentication tests. |
| eng/ProjectReferences.props | Registers the new project for reference resolution. |
| docs/list-of-diagnostics.md | Documents ASP0030 experimental diagnostic for DBSC APIs. |
| AspNetCore.slnx | Adds DeviceBoundSessions projects to the solution structure. |
- remove the temporary 20-minute ExpireTimeSpan used for manual sliding tests 🔒 - Generated by Copilot
- point AspNetCore.slnx at the renamed DbscDebugServer sample - remove the non-existent DeviceBoundSessions test project (tests live in the shared Authentication.Test project) 🔒 - Generated by Copilot
- run GenerateProjectList to fix ordering/formatting of the DBSC entry - updates ProjectReferences, ShippingAssemblies, and TrimmableProjects 🔒 - Generated by Copilot
…wiring Resolve correctness issues raised in PR review and tighten the handler's dependency wiring: - Mirror the source sign-in's IsPersistent on the refresh cookie instead of forcing it persistent, so DBSC no longer changes session/persistence semantics for session-only sign-ins. - Derive the session instruction's credential name and attributes from the session scheme's CookieAuthenticationOptions so they stay in lock-step with the cookie actually emitted; throw on a missing scheme/name rather than emitting a guessed default that can never match. - Derive the refresh cookie path from the configured RefreshPath so a customized RefreshPath still receives the cookie. - Resolve the session scheme's cookie name at request time in the policy scheme selector so a custom Cookie.Name is still matched. - Constructor-inject IOptionsMonitor<CookieAuthenticationOptions> instead of service-locating it per request; update PublicAPI.Unshipped accordingly. - Use the idiomatic JsonSerializer.Deserialize<JsonElement> in the proof-key test helper instead of an undisposed JsonDocument. 🤖 Generated by Copilot
DBSC ships as a standalone NuGet package (it is not part of the Microsoft.AspNetCore.App shared framework) and its public surface is entirely experimental (ASP0030). Set VersionPrefix to the repo's ExperimentalVersionPrefix so it publishes as a 0.x preview (0.11.0) rather than the stable framework version (11.0.0), matching the convention used by the experimental Microsoft.AspNetCore.Grpc.Swagger package and signaling that the API is not yet stable. 🤖 Generated by Copilot
|
@copilot resolve the merge conflicts in this pull request |
The merge conflicts have been resolved locally — the only conflict was the |
- reset MessagePack-CSharp and googletest gitlinks to match main - resolves merge conflict in PR #67388
- restore MessagePack-CSharp and googletest gitlinks to branch point so PR touches neither
353a815 to
d98dcb1
Compare
| await Context.SignInAsync(Options.SessionScheme, principal, sessionProperties); | ||
|
|
||
| // 3. Delete the long-lived source cookie (exchange complete) | ||
| await Context.SignOutAsync(Options.RegistrationSourceScheme); |
There was a problem hiding this comment.
After this succeeds, the source cookie is gone and the live auth state is in the DBSC refresh/session cookies. Does a later SignOutAsync(Options.RegistrationSourceScheme) clear those derived cookies too? It looks like logout would only delete the already-removed source cookie.
Since app code does not need to know the derived scheme names to create these cookies, it should not need to know them to clear them.
| } | ||
| else if (schemes.SessionSchemes.TryGetValue(name, out var sessionSourceScheme)) | ||
| { | ||
| CopyFromSource(sessionSourceScheme, options); |
There was a problem hiding this comment.
We copy cookie attributes from the source scheme, but not its validation/revocation behavior. After DBSC registration, Identity's OnValidatePrincipal security-stamp check and any source SessionStore revocation are bypassed by the session/refresh cookies. Can we preserve those semantics? I think we might just have to copy over the event handlers from the source cookie options.
| builder.Services.Configure<AuthenticationOptions>(o => | ||
| { | ||
| o.DefaultAuthenticateScheme = policyScheme; | ||
| o.DefaultSignInScheme = sourceScheme; |
There was a problem hiding this comment.
I don't think AddDeviceBoundSession should automatically set the specified sourceScheme as the DefaultSignInScheme. Nor should it set DefaultAuthenticationScheme . These are things that should be configured explicitly.
I know we have logic where if you have a single scheme, we treat that as the default, and this never will be a single scheme, but I don't think this is the solution.
We could have had our OIDC handler automatically configure itself as the default sign in scheme, but we don't, because we prefer to be explicit.
Identity does set default schemes in its extension methods, but it's the only thing to do so, and it's special since it takes more complete ownership of the user's Identity. None of our authentication handlers should change default schemes on their own.
| /// <summary> | ||
| /// Gets the list of allowed refresh initiator host patterns. | ||
| /// </summary> | ||
| public IList<string> AllowedRefreshInitiators { get; } = new List<string>(); |
There was a problem hiding this comment.
AlowedRefreshInitiators never appears in the JSON as allowed_refresh_initiators, so setting the option has no effect. I assume we want to keep this setting, but we just weren't flowing it? Can we add a test for this?
| return new SessionInstruction | ||
| { | ||
| SessionIdentifier = sessionId, | ||
| RefreshUrl = Options.RefreshPath.Value, |
There was a problem hiding this comment.
Should this include OriginalPathBase, like AuthenticationHandler<T>.BuildRedirectUri(...) does for remote auth callback URLs? Maybe we should just use it?
I think the Secure-Session-Registration path has the same issue, otherwise an app mounted under /foo tells the browser to POST to /.well-known/dbsc/... instead of /foo/.well-known/dbsc/...
We should add some tests with a non-default path base too.
CopyFromSource saved and restored target.Cookie.Name/Path around the attribute copy, but the copy never touches Name or Path, so the save-restore was a no-op. Remove it; callers set the refresh cookie path explicitly and the session scheme keeps its configured name. Addresses PR review feedback. 🤖 Generated by Copilot
Device Bound Session Credentials (DBSC) for cookie authentication (prototype)
What this adds
A new experimental authentication component,
Microsoft.AspNetCore.Authentication.DeviceBoundSessions, that implements the server side of Device Bound Session Credentials. DBSC binds a browser session to a device-held private key: the short-lived session cookie is periodically refreshed only when the browser can produce a fresh signed proof-of-possession, so a stolen cookie alone is useless.It is designed as a drop-in hardening layer over an existing cookie auth scheme: you point it at a "source" sign-in scheme and it manages the registration handshake, a path-scoped refresh cookie, and a short-lived session cookie.
Shape of the API
All public types carry
[Experimental("ASP0030")]; consumers must explicitly opt in.How it works
/.well-known/dbsc/registration): validates the browser's signed proof JWT, binds it to the authenticated principal via a data-protected, time-limited challenge, then stamps a path-scoped refresh cookie (carrying the device public key + session id) and a short-lived session cookie./.well-known/dbsc/refresh): authenticates the refresh cookie, issues aSecure-Session-Challenge(403), validates the returned proof JWT signature against the bound key and the challenge binding, then re-mints the short-lived session cookie.ExpireTimeSpan/SlidingExpirationand slides exactly like the auth cookie it replaces (no session-lifetime regression when DBSC is enabled). The session cookie stays deliberately short-lived and non-sliding.LoggerMessageevents for every validation failure (challenge undecryptable/malformed/mismatch, proof malformed/wrong-typ/unsupported-alg/bad-signature), atDebug, never logging secrets.Components
src/Security/Authentication/DeviceBoundSessions/src/DbscDebugServersample with a live dashboard that decodes cookies, proofs, and challenges for inspectionTesting
src/Security/Authentication/test/DeviceBoundSessions/— 38 tests covering challenge round-trips and domain separation, JWT proof validation, data-protection of derived cookies, and refresh-cookie sliding inheritance.DbscDebugServersample.Open questions for review
ASP0030is reserved indocs/list-of-diagnostics.md; confirm allocation.attributesadvertised in the session instruction are currently hard-coded rather than derived from the session scheme's cookie builder./plans/dbsc-design.mdis residual planning document which shall be removed before merge, we keeping it for now for reviewersReferences
Specification
Status: Editor's Draft, 17 April 2026 — "may change at any moment." Lowest standardization maturity.
Browser implementation & documentation
chrome://flags/#enable-standard-device-bound-session-credentialsChromium source code
TODO
/plans/dbsc-design.md