File: DeviceBoundSessionExtensions.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.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.DeviceBoundSessions;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using DbscOptions = Microsoft.AspNetCore.Authentication.DeviceBoundSessions.DeviceBoundSessionOptions;
 
namespace Microsoft.Extensions.DependencyInjection;
 
/// <summary>
/// Extension methods to configure Device Bound Session Credentials authentication.
/// </summary>
[Experimental("ASP0031", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
public static class DeviceBoundSessionExtensions
{
    /// <summary>
    /// 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.
    /// </summary>
    /// <param name="builder">The authentication builder.</param>
    /// <param name="sourceScheme">The existing cookie authentication scheme to protect with DBSC (e.g., "Identity.Application").</param>
    /// <returns>The authentication builder for chaining.</returns>
    public static AuthenticationBuilder AddDeviceBoundSession(
        this AuthenticationBuilder builder,
        string sourceScheme)
        => AddDeviceBoundSessionCore(builder, DeviceBoundSessionDefaults.AuthenticationScheme, sourceScheme, configureOptions: null);
 
    /// <summary>
    /// Adds Device Bound Session Credentials (DBSC) authentication that wraps an existing cookie scheme.
    /// </summary>
    /// <param name="builder">The authentication builder.</param>
    /// <param name="sourceScheme">The existing cookie authentication scheme to protect with DBSC (e.g., "Identity.Application").</param>
    /// <param name="configureOptions">Action to configure DBSC options.</param>
    /// <returns>The authentication builder for chaining.</returns>
    public static AuthenticationBuilder AddDeviceBoundSession(
        this AuthenticationBuilder builder,
        string sourceScheme,
        Action<DbscOptions> configureOptions)
        => AddDeviceBoundSessionCore(builder, DeviceBoundSessionDefaults.AuthenticationScheme, sourceScheme, configureOptions);
 
    /// <summary>
    /// Adds Device Bound Session Credentials (DBSC) authentication that wraps an existing cookie scheme.
    /// </summary>
    /// <param name="builder">The authentication builder.</param>
    /// <param name="authenticationScheme">The DBSC authentication scheme name.</param>
    /// <param name="sourceScheme">The existing cookie authentication scheme to protect with DBSC.</param>
    /// <returns>The authentication builder for chaining.</returns>
    public static AuthenticationBuilder AddDeviceBoundSession(
        this AuthenticationBuilder builder,
        string authenticationScheme,
        string sourceScheme)
        => AddDeviceBoundSessionCore(builder, authenticationScheme, sourceScheme, configureOptions: null);
 
    /// <summary>
    /// Adds Device Bound Session Credentials (DBSC) authentication that wraps an existing cookie scheme.
    /// </summary>
    /// <param name="builder">The authentication builder.</param>
    /// <param name="authenticationScheme">The DBSC authentication scheme name.</param>
    /// <param name="sourceScheme">The existing cookie authentication scheme to protect with DBSC.</param>
    /// <param name="configureOptions">Action to configure DBSC options.</param>
    /// <returns>The authentication builder for chaining.</returns>
    public static AuthenticationBuilder AddDeviceBoundSession(
        this AuthenticationBuilder builder,
        string authenticationScheme,
        string sourceScheme,
        Action<DbscOptions> configureOptions)
        => AddDeviceBoundSessionCore(builder, authenticationScheme, sourceScheme, configureOptions);
 
    private static AuthenticationBuilder AddDeviceBoundSessionCore(
        AuthenticationBuilder builder,
        string authenticationScheme,
        string sourceScheme,
        Action<DbscOptions>? configureOptions)
    {
        var refreshScheme = $"{sourceScheme}.Dbsc.Refresh";
        var sessionScheme = $"{sourceScheme}.Dbsc.Session";
        var policyScheme = $"{sourceScheme}.Dbsc";
 
        // Add the refresh cookie scheme — settings will be copied from the source
        // scheme via PostConfigureDeviceBoundSessionDerivedCookieOptions
        builder.AddCookie(refreshScheme, o =>
        {
            o.Cookie.Name = $".AspNetCore.{sourceScheme}.Dbsc.Refresh";
            o.Cookie.Path = "/.well-known/dbsc";
        });
 
        // Add the session cookie scheme — settings copied from source, expiry overridden
        builder.AddCookie(sessionScheme, o =>
        {
            o.Cookie.Name = $".AspNetCore.{sourceScheme}.Dbsc.Session";
        });
 
        // Add a policy scheme that tries the session cookie first, then falls back to the source scheme
        builder.AddPolicyScheme(policyScheme, policyScheme, o =>
        {
            o.ForwardDefaultSelector = context =>
            {
                // Resolve the session scheme's configured cookie name at request time so an app that
                // customizes CookieAuthenticationOptions.Cookie.Name is still matched. Otherwise the
                // selector would miss the cookie and fall back to a source scheme that DBSC registration
                // may have deleted, effectively logging the user out.
                var sessionCookieName = context.RequestServices
                    .GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>()
                    .Get(sessionScheme).Cookie.Name ?? $".AspNetCore.{sessionScheme}";
                if (context.Request.Cookies.ContainsKey(sessionCookieName))
                {
                    return sessionScheme;
                }
                return sourceScheme;
            };
        });
 
        // Register services
        builder.Services.TryAddSingleton<DeviceBoundSessionChallengeProtector>();
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureDeviceBoundSessionCookieOptions>());
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureDeviceBoundSessionDerivedCookieOptions>());
        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<AuthenticationOptions>, PostConfigureDeviceBoundSessionAuthenticationOptions>());
        builder.Services.Configure<DeviceBoundSessionSourceSchemes>(o =>
        {
            o.Schemes[sourceScheme] = authenticationScheme;
            o.RefreshSchemes[refreshScheme] = sourceScheme;
            o.SessionSchemes[sessionScheme] = sourceScheme;
            o.PolicySchemes[sourceScheme] = policyScheme;
        });
 
        // Add the DBSC protocol handler
        builder.AddScheme<DbscOptions, DeviceBoundSessionHandler>(authenticationScheme, o =>
        {
            o.RegistrationSourceScheme = sourceScheme;
            o.RefreshScheme = refreshScheme;
            o.SessionScheme = sessionScheme;
            configureOptions?.Invoke(o);
        });
 
        return builder;
    }
}