Skip to content

[API Proposal]: Device Bound Session Credentials (DBSC) for cookie authentication #68117

Description

@javiercn

Background and Motivation

Cookie-based sign-in today produces a bearer credential: whoever holds the cookie is the user. If the cookie is exfiltrated (infostealer malware, token theft), it can be replayed from any machine for the remaining lifetime of the session. Device Bound Session Credentials (DBSC) is a W3C spec that binds a browser session to a device-held private key: the browser proves possession of the key to obtain a short-lived cookie, so a stolen cookie stops working once it expires.

This proposal is the public surface for a new standalone, experimental package, Microsoft.AspNetCore.Authentication.DeviceBoundSessions, tracked by #66478 and implemented in #67388. It "[implements] the DBSC protocol (W3C spec) in the cookie authentication handler" using a "stateless two-cookie pattern": a long-lived cookie carrying the auth ticket plus the embedded public key (refresh-token equivalent) and a short-lived .Dbsc.Session cookie that is the device-bound credential (access-token equivalent), with registration and refresh endpoints handled by an IAuthenticationRequestHandler.

The design goal is that an app opts into DBSC by wrapping an existing cookie schemeAddDeviceBoundSession(sourceScheme) — rather than rewriting its authentication setup. The primary target is ASP.NET Core Identity, whose application cookie (IdentityConstants.ApplicationScheme) is exactly the long-lived bearer credential DBSC is meant to protect. The handler derives the refresh/session/policy schemes, copies cookie settings from the source scheme, and routes authentication through a policy scheme, so app code and [Authorize] continue to work unchanged.

The API ships as a standalone NuGet package (not the shared framework) because it carries a third-party dependency (Microsoft.IdentityModel.JsonWebTokens) and System.Formats.Cbor, and the entire surface is gated behind the experimental diagnostic ASP0031 ("Experimental warning for Device Bound Sessions (DBSC) APIs") and versioned with ExperimentalVersionPrefix as a 0.x preview package.

Proposed API

Everything below is new API in a new assembly/package, Microsoft.AspNetCore.Authentication.DeviceBoundSessions. Nothing is added to or changed in an existing assembly, so there is no diff against a current surface. Every type is annotated [Experimental("ASP0031", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")].

New static class DeviceBoundSessionExtensions, the DI entry point:

namespace Microsoft.Extensions.DependencyInjection;

public static class DeviceBoundSessionExtensions
{
    public static AuthenticationBuilder AddDeviceBoundSession(this AuthenticationBuilder builder, string sourceScheme);
    public static AuthenticationBuilder AddDeviceBoundSession(this AuthenticationBuilder builder, string sourceScheme, Action<DeviceBoundSessionOptions> configureOptions);
    public static AuthenticationBuilder AddDeviceBoundSession(this AuthenticationBuilder builder, string authenticationScheme, string sourceScheme);
    public static AuthenticationBuilder AddDeviceBoundSession(this AuthenticationBuilder builder, string authenticationScheme, string sourceScheme, Action<DeviceBoundSessionOptions> configureOptions);
}

New types in the feature namespace — the defaults class, the HttpContext extension used to opt already-signed-in users into DBSC, the protocol handler, its options, and the options-side scope rule:

namespace Microsoft.AspNetCore.Authentication.DeviceBoundSessions;

public static class DeviceBoundSessionDefaults
{
    public const string AuthenticationScheme = "DeviceBoundSession";
    public const string RegistrationPath = "/.well-known/dbsc/registration";
    public const string RefreshPath = "/.well-known/dbsc/refresh";
}

public static class DeviceBoundSessionHttpContextExtensions
{
    public static void WriteDeviceBoundSessionRegistration(this HttpContext context, string sourceScheme);
}

public class DeviceBoundSessionHandler : AuthenticationHandler<DeviceBoundSessionOptions>, IAuthenticationRequestHandler
{
    public DeviceBoundSessionHandler(
        IOptionsMonitor<DeviceBoundSessionOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        IDataProtectionProvider dataProtectionProvider,
        IOptionsMonitor<CookieAuthenticationOptions> cookieOptionsMonitor);

    public Task<bool> HandleRequestAsync();
    protected override Task<AuthenticateResult> HandleAuthenticateAsync();
}

public class DeviceBoundSessionOptions : AuthenticationSchemeOptions
{
    public DeviceBoundSessionOptions();

    public string? RegistrationSourceScheme { get; set; }
    public string? RefreshScheme { get; set; }
    public string? SessionScheme { get; set; }
    public PathString RegistrationPath { get; set; }          // "/.well-known/dbsc/registration"
    public PathString RefreshPath { get; set; }               // "/.well-known/dbsc/refresh"
    public TimeSpan ShortLivedCookieExpiration { get; set; }  // 10 minutes
    public TimeSpan ChallengeMaxAge { get; set; }             // 5 minutes
    public bool IncludeSite { get; set; }                     // false
    public IList<DeviceBoundSessionScopeRule> ScopeSpecifications { get; }
    public IList<string> AllowedRefreshInitiators { get; }

    public override void Validate(string scheme);
}

public class DeviceBoundSessionScopeRule
{
    public DeviceBoundSessionScopeRule();
    public string Type { get; set; }    // "include"
    public string Domain { get; set; }  // "*"
    public string Path { get; set; }    // "/"
}

New types modelling the W3C wire formats that are serialized to the browser during registration and refresh:

namespace Microsoft.AspNetCore.Authentication.DeviceBoundSessions;

// W3C DBSC §9.6 "JSON Session Instruction Format"
public sealed class SessionInstruction
{
    public SessionInstruction();
    public string SessionIdentifier { get; set; }
    public string? RefreshUrl { get; set; }
    public bool Continue { get; set; }                            // true
    public SessionScope? Scope { get; set; }
    public List<SessionCredential>? Credentials { get; set; }
    public List<string> AllowedRefreshInitiators { get; set; }
}

// W3C DBSC §9.7 "JSON Session Scope Instruction Format"
public sealed class SessionScope
{
    public SessionScope();
    public string? Origin { get; set; }
    public bool IncludeSite { get; set; }
    public List<SessionScopeRule> ScopeSpecification { get; set; }
}

// W3C DBSC §9.8 "JSON Session Scope Rule Format"
public sealed class SessionScopeRule
{
    public SessionScopeRule();
    public string Type { get; set; }
    public string Domain { get; set; }
    public string Path { get; set; }
}

// W3C DBSC §9.9 "JSON Session Credentials Format"
public sealed class SessionCredential
{
    public SessionCredential();
    public string Type { get; }              // always "cookie"
    public required string Name { get; set; }
    public string Attributes { get; set; }   // ""
}

Implementation reference: PR #67388 (commit eb04e4399c), in particular src/Security/Authentication/DeviceBoundSessions/src/PublicAPI.Unshipped.txt.

Usage Examples

The main scenario is an ASP.NET Core Identity app protecting its application cookie. DBSC wraps the existing Identity scheme; nothing else about the app's authentication setup changes:

#pragma warning disable ASP0031

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDefaultIdentity<IdentityUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

// Bind the Identity application cookie to the device.
builder.Services.AddAuthentication()
    .AddDeviceBoundSession(IdentityConstants.ApplicationScheme);

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();

app.Run();

Existing app code is unaffected — [Authorize], SignInManager, and User.Identity keep working, because authentication is routed through the policy scheme that AddDeviceBoundSession registers:

[Authorize]
public class IndexModel : PageModel
{
    public void OnGet() => Name = User.Identity!.Name;

    public string? Name { get; private set; }
}

Tuning the bound-cookie lifetime, challenge freshness, and session scope:

builder.Services.AddAuthentication()
    .AddDeviceBoundSession(IdentityConstants.ApplicationScheme, options =>
    {
        options.ShortLivedCookieExpiration = TimeSpan.FromMinutes(10);
        options.ChallengeMaxAge = TimeSpan.FromMinutes(5);
        options.RegistrationPath = "/.well-known/dbsc/registration";
        options.RefreshPath = "/.well-known/dbsc/refresh";
        options.IncludeSite = true;
        options.AllowedRefreshInitiators.Add("app.contoso.com");
        options.ScopeSpecifications.Add(new DeviceBoundSessionScopeRule
        {
            Type = "exclude",
            Domain = "*",
            Path = "/public",
        });
    });

Migrating users who are already signed in with an Identity cookie, without forcing a re-login. WriteDeviceBoundSessionRegistration emits the Secure-Session-Registration header for the current authenticated principal; DBSC-capable browsers start registration on the next response, and browsers that don't support DBSC ignore it:

// The DBSC bound session cookie follows .AspNetCore.{sourceScheme}.Dbsc.Session
const string boundCookie = ".AspNetCore.Identity.Application.Dbsc.Session";

app.Use(async (context, next) =>
{
    var alreadyBound = context.Request.Cookies.ContainsKey(boundCookie);
    var alreadyOffered = context.Request.Cookies.ContainsKey("dbsc-offered");

    if (context.User.Identity?.IsAuthenticated == true && !alreadyBound && !alreadyOffered)
    {
        context.WriteDeviceBoundSessionRegistration(IdentityConstants.ApplicationScheme);

        context.Response.Cookies.Append("dbsc-offered", "1", new CookieOptions
        {
            Path = "/",
            Secure = true,
            HttpOnly = true,
            SameSite = SameSiteMode.Lax,
        });
    }

    await next();
});

Alternative Designs

  • Implementing DBSC inside the existing cookie handler vs. a separate scheme. An earlier iteration lived in Microsoft.AspNetCore.Authentication.Cookies; it was removed in favor of a dedicated handler project ("Remove v1 DBSC implementation from Cookies project", "Add DeviceBoundSessions handler project (v2 architecture)"). The current design instead composes existing building blocks: the DBSC handler owns only the protocol endpoints, while cookie handling is delegated to derived cookie schemes and a policy scheme selects between the bound session cookie and the source cookie.

  • Shared framework vs. standalone package. The feature was explicitly changed to "ship as NuGet package, not shared framework", because it takes a dependency on Microsoft.IdentityModel.JsonWebTokens for JWT proof validation and on System.Formats.Cbor.

  • Stateful vs. stateless session storage. The implementation uses a "stateless two-cookie pattern" with "stateless challenge generation via DataProtection", so no server-side session store is required. Server-side revocation via a shared ITicketStore is tracked separately in DBSC: share source scheme ITicketStore (server-side revocation) with derived cookies #67794.

  • Explicit scheme names vs. derived scheme names. Rather than making an Identity app declare four schemes, AddDeviceBoundSession derives {sourceScheme}.Dbsc.Refresh, {sourceScheme}.Dbsc.Session, and {sourceScheme}.Dbsc, and post-configures the default scheme so "the app never has to know (or wire) the derived DBSC scheme names". RegistrationSourceScheme / RefreshScheme / SessionScheme remain on the options as an escape hatch.

  • Advertised endpoint URLs vs. local handler paths. RegistrationPath / RefreshPath are deliberately restricted to nonempty, root-relative, application-local paths (no full URLs, network-path references, query strings, or fragments), even though "the DBSC protocol permits absolute and cross-origin same-site endpoint references". Splitting the advertised URL from the local handler path is tracked in DBSC: separate browser-advertised endpoint URLs from local handler paths #67850.

Risks

  • Experimental surface. Everything is annotated [Experimental("ASP0031")] and the package uses ExperimentalVersionPrefix (0.x), so consumers must suppress ASP0031 to use it and the shape is expected to change. ASP0031 is a new diagnostic ID reserved in docs/list-of-diagnostics.md.

  • New public API in a new package, so no breaking changes to existing assemblies; the ref-assembly delta is additive and lives entirely in the new assembly.

  • Scheme-name collisions. The derived scheme and cookie names are computed from the source scheme name ({sourceScheme}.Dbsc.*); an app that already registered a scheme with one of those names would conflict.

  • Security-sensitive defaults and deployment requirements. The registration header "is only honored over HTTPS", the callable API requires an authenticated principal, and correct operation behind a proxy requires trusted forwarded scheme/host/prefix handling before authentication and no permissive credentialed CORS on the refresh endpoint. Mis-deployment weakens the binding the feature exists to provide.

  • Caller-owned idempotency for WriteDeviceBoundSessionRegistration. "Emitting it repeatedly mints a new challenge each time, so the caller owns idempotency" — a naive call on every response would issue a fresh challenge per request.

  • IncludeSite = true has a topology constraint: it "requires the registration endpoint to be served from the registrable-domain host because the scope origin is derived from the registration request origin". Registration from subdomains is tracked in DBSC: support site scope registration from subdomains #67827.

  • Wire-format DTOs are public and mutable. SessionInstruction, SessionScope, SessionScopeRule, and SessionCredential mirror the W3C JSON shapes with get; set; properties and List<T> members; if the spec evolves, these types evolve with it.

  • Public unsealed handler/options types (DeviceBoundSessionHandler, DeviceBoundSessionOptions, DeviceBoundSessionScopeRule) are extensible by derivation, which constrains future changes once the experimental gate is removed.


Source justification (per .github/prompts/ApiReview.prompt.md)

Background and Motivation

  • DBSC implemented in cookie auth with a two-cookie pattern: "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) / Registration/refresh middleware / ES256/RS256 JWT validation / Stateless challenge generation via DataProtection" (commit eb04e4399c).
  • Wraps an existing cookie scheme: "Adds Device Bound Session Credentials (DBSC) authentication that wraps an existing cookie scheme. This sets up: a policy scheme (default auth), a refresh cookie scheme (path-scoped stash), a session cookie scheme (short-lived), and the DBSC protocol handler." (DeviceBoundSessionExtensions.cs).
  • Identity as the target scheme: "The existing cookie authentication scheme to protect with DBSC (e.g., "Identity.Application")" (DeviceBoundSessionExtensions.cs) and "The source cookie authentication scheme that was passed to AddDeviceBoundSession (for example IdentityConstants.ApplicationScheme)" (DeviceBoundSessionHttpContextExtensions.cs).
  • Policy-scheme routing: "Policy scheme routes auth: tries Session cookie first, falls back to source" (commit eb04e4399c).
  • Standalone experimental package: "Experimental (ASP0031); ships as a 0.x preview package, matching the convention for experimental standalone packages." and <Reference Include="Microsoft.IdentityModel.JsonWebTokens" />, <Reference Include="System.Formats.Cbor" /> (Microsoft.AspNetCore.Authentication.DeviceBoundSessions.csproj).
  • Diagnostic reservation: "__ASP0031__ | Experimental warning for Device Bound Sessions (DBSC) APIs" (docs/list-of-diagnostics.md).
  • Package description: "ASP.NET Core middleware that enables Device Bound Session Credentials (DBSC) over an inner authentication scheme." (csproj).

Proposed API

  • Entire surface transcribed from src/Security/Authentication/DeviceBoundSessions/src/PublicAPI.Unshipped.txt, e.g. "static Microsoft.Extensions.DependencyInjection.DeviceBoundSessionExtensions.AddDeviceBoundSession(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, string! sourceScheme) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder!" and "const Microsoft.AspNetCore.Authentication.DeviceBoundSessions.DeviceBoundSessionDefaults.RegistrationPath = "/.well-known/dbsc/registration" -> string!".
  • New assembly (no existing-surface diff): the API entries live in a newly added PublicAPI.Unshipped.txt for a new project, "src/Security/Authentication/DeviceBoundSessions/src/Microsoft.AspNetCore.Authentication.DeviceBoundSessions.csproj" (commit eb04e4399c file list).
  • Experimental annotation on every type: "[Experimental("ASP0031", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]" (all public source files).
  • Handler base/interface: "public class DeviceBoundSessionHandler : AuthenticationHandler, IAuthenticationRequestHandler" (DeviceBoundSessionHandler.cs).
  • Defaults for options: "public TimeSpan ShortLivedCookieExpiration { get; set; } = TimeSpan.FromMinutes(10);", "public TimeSpan ChallengeMaxAge { get; set; } = TimeSpan.FromMinutes(5);" (DeviceBoundSessionOptions.cs).
  • W3C section mapping: "Corresponds to the "JSON Session Instruction Format" defined in W3C Device Bound Session Credentials §9.6", "§9.7", "§9.8", "§9.9" (SessionInstruction.cs, SessionScope.cs, SessionScopeRule.cs, SessionCredential.cs).
  • Credential shape: "public string Type { get; } = "cookie";", "public required string Name { get; set; }" (SessionCredential.cs).

Usage Examples

  • Registration/migration middleware sample is taken verbatim from the <example> block on WriteDeviceBoundSessionRegistration (DeviceBoundSessionHttpContextExtensions.cs), which itself uses IdentityConstants.ApplicationScheme and the .AspNetCore.Identity.Application.Dbsc.Session cookie name.
  • Wrapping the Identity cookie scheme follows the documented parameter contract "The existing cookie authentication scheme to protect with DBSC (e.g., "Identity.Application")" (DeviceBoundSessionExtensions.cs); the sample app demonstrates the same shape against its own source scheme: "services.AddAuthentication(DbscNames.Source) .AddCookie(DbscNames.Source, ...) ... .AddDeviceBoundSession(...)" (samples/DbscDebugServer/Program.cs).
  • Suppression pragma: "#pragma warning disable ASP0031 // Type is for evaluation purposes only and is subject to change or removal in future updates." (samples/DbscDebugServer/Program.cs).
  • Unchanged app code: "The app makes the source cookie its default scheme; AddDeviceBoundSession detects that and redirects the default authenticate scheme to its policy scheme, so the app never has to know (or wire) the derived DBSC scheme names." (samples/DbscDebugServer/Program.cs).

Alternative Designs

  • Handler split out of Cookies: "Remove v1 DBSC implementation from Cookies project" and "WIP: Add DeviceBoundSessions handler project (v2 architecture)" (commit history).
  • Packaging decision: "Fix DBSC project to ship as NuGet package, not shared framework" (commit history).
  • Stateless design: "Stateless challenge generation via DataProtection" (commit eb04e4399c); server-side revocation deferred per DBSC: share source scheme ITicketStore (server-side revocation) with derived cookies #67794 "DBSC: share source scheme ITicketStore (server-side revocation) with derived cookies".
  • Derived scheme names: "var refreshScheme = $"{sourceScheme}.Dbsc.Refresh"; var sessionScheme = $"{sourceScheme}.Dbsc.Session"; var policyScheme = $"{sourceScheme}.Dbsc";" (DeviceBoundSessionExtensions.cs).
  • Path restrictions: "Although the DBSC protocol permits absolute and cross-origin same-site endpoint references, they are outside this component's local request-handler model." (DeviceBoundSessionOptions.cs); follow-up DBSC: separate browser-advertised endpoint URLs from local handler paths #67850 "DBSC: separate browser-advertised endpoint URLs from local handler paths".

Risks

  • HTTPS-only and idempotency: "Emitting it repeatedly mints a new challenge each time, so the caller owns idempotency" and "The header is only honored over HTTPS." (DeviceBoundSessionHttpContextExtensions.cs).
  • Authenticated-principal requirement: "The Device Bound Session registration challenge must be bound to an authenticated principal." (DeviceBoundSessionHttpContextExtensions.cs).
  • Proxy/CORS deployment guidance: "Production deployments behind a proxy must apply trusted forwarded scheme, host, and prefix before authentication... Do not enable permissive credentialed CORS on the refresh endpoint." (samples/DbscDebugServer/Program.cs).
  • IncludeSite constraint: "With the current implementation, true requires the registration endpoint to be served from the registrable-domain host because the scope origin is derived from the registration request origin." (DeviceBoundSessionOptions.cs); follow-up DBSC: support site scope registration from subdomains #67827 "DBSC: support site scope registration from subdomains".
  • Experimental versioning: "$(ExperimentalVersionPrefix)" (csproj).

/cc #66478 #67388

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-proposalapi-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-authIncludes: Authn, Authz, OAuth, OIDC, Bearer

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions