| File: DeviceBoundSessionHttpContextExtensions.cs | Web Access |
| Project: src\aspnetcore\src\Security\Authentication\DeviceBoundSessions\src\Microsoft.AspNetCore.Authentication.DeviceBoundSessions.csproj (Microsoft.AspNetCore.Authentication.DeviceBoundSessions) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Authentication.DeviceBoundSessions; /// <summary> /// Extension methods on <see cref="HttpContext"/> for Device Bound Session Credentials (DBSC). /// </summary> [Experimental("ASP0031", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] public static class DeviceBoundSessionHttpContextExtensions { /// <summary> /// Writes the DBSC <c>Secure-Session-Registration</c> response header for the current /// authenticated user (<see cref="HttpContext.User"/>), advertising the supported signing /// algorithms and a fresh, principal-bound registration challenge. /// </summary> /// <remarks> /// <para> /// Use this to opt an <em>already signed-in</em> user into a device bound session without forcing a /// re-login — for example, to migrate existing sessions after deploying DBSC instead of waiting /// for them to expire. Registration is not tied to the sign-in event: a DBSC-capable browser reacts /// to this header on any authenticated response and starts registration, sending the proof to the /// registration endpoint with the existing source cookie attached. Browsers that do not support DBSC /// ignore the header, so it is safe to emit broadly. /// </para> /// <para> /// Calling this does not itself create a session; it only advertises registration. Emitting it /// repeatedly mints a new challenge each time, so the caller owns idempotency: gate the call so a /// user is offered registration at most once (for example, with a durable marker cookie), and skip /// clients that are already bound (detected by the presence of the bound session cookie). The header /// is only honored over HTTPS. /// </para> /// </remarks> /// <param name="context">The current <see cref="HttpContext"/>.</param> /// <param name="sourceScheme"> /// The source cookie authentication scheme that was passed to /// <c>AddDeviceBoundSession</c> (for example <c>IdentityConstants.ApplicationScheme</c>). /// </param> /// <exception cref="ArgumentNullException"><paramref name="context"/> is <see langword="null"/>.</exception> /// <exception cref="ArgumentException"><paramref name="sourceScheme"/> is <see langword="null"/> or empty.</exception> /// <exception cref="InvalidOperationException"> /// <paramref name="sourceScheme"/> is not a registered Device Bound Session source scheme, or the /// current user is not authenticated. /// </exception> /// <example> /// Migration middleware that offers DBSC registration to already-signed-in users once each. It skips /// clients that already have a bound session (the bound session cookie is present) and uses a durable /// marker cookie so the offer is not repeated on every request: /// <code> /// // 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(); /// }); /// </code> /// </example> public static void WriteDeviceBoundSessionRegistration(this HttpContext context, string sourceScheme) { ArgumentNullException.ThrowIfNull(context); ArgumentException.ThrowIfNullOrEmpty(sourceScheme); var principal = context.User; if (principal.Identity?.IsAuthenticated != true) { throw new InvalidOperationException( "The Device Bound Session registration challenge must be bound to an authenticated principal."); } var sourceSchemes = context.RequestServices .GetRequiredService<IOptions<DeviceBoundSessionSourceSchemes>>().Value; if (!sourceSchemes.Schemes.TryGetValue(sourceScheme, out var dbscScheme)) { throw new InvalidOperationException( $"'{sourceScheme}' is not registered as a Device Bound Session source scheme. " + $"Call AddDeviceBoundSession(\"{sourceScheme}\") to register it first."); } DeviceBoundSessionRegistrationHeader.Emit(context, principal, dbscScheme); } }