File: DeviceBoundSessionCookieEvents.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.
 
#pragma warning disable ASP0031 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Authentication.DeviceBoundSessions;
 
/// <summary>
/// A <see cref="CookieAuthenticationEvents"/> decorator used when a DBSC source cookie scheme sets
/// the cookie options' <c>EventsType</c>. It emits the DBSC registration header on
/// sign-in and forwards every event to the application's configured (DI-resolved) inner events.
/// </summary>
internal sealed class DeviceBoundSessionCookieEvents : CookieAuthenticationEvents
{
    private readonly string _dbscScheme;
    private readonly CookieAuthenticationEvents _innerEvents;
    private readonly Type? _innerEventsType;
 
    public DeviceBoundSessionCookieEvents(string dbscScheme, CookieAuthenticationEvents innerEvents, Type? innerEventsType)
    {
        _dbscScheme = dbscScheme;
        _innerEvents = innerEvents;
        _innerEventsType = innerEventsType;
    }
 
    public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
    {
        await GetInnerEvents(context.HttpContext).ValidatePrincipal(context);
    }
 
    public override async Task CheckSlidingExpiration(CookieSlidingExpirationContext context)
    {
        await GetInnerEvents(context.HttpContext).CheckSlidingExpiration(context);
    }
 
    public override async Task SigningIn(CookieSigningInContext context)
    {
        await GetInnerEvents(context.HttpContext).SigningIn(context);
        DeviceBoundSessionRegistrationHeader.Emit(context.HttpContext, context.Principal, _dbscScheme);
    }
 
    public override async Task SignedIn(CookieSignedInContext context)
    {
        await GetInnerEvents(context.HttpContext).SignedIn(context);
    }
 
    public override async Task SigningOut(CookieSigningOutContext context)
    {
        await ClearDerivedCookiesAsync(context.HttpContext, _dbscScheme);
        await GetInnerEvents(context.HttpContext).SigningOut(context);
    }
 
    public override async Task RedirectToLogout(RedirectContext<CookieAuthenticationOptions> context)
    {
        await GetInnerEvents(context.HttpContext).RedirectToLogout(context);
    }
 
    public override async Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
    {
        await GetInnerEvents(context.HttpContext).RedirectToLogin(context);
    }
 
    public override async Task RedirectToReturnUrl(RedirectContext<CookieAuthenticationOptions> context)
    {
        await GetInnerEvents(context.HttpContext).RedirectToReturnUrl(context);
    }
 
    public override async Task RedirectToAccessDenied(RedirectContext<CookieAuthenticationOptions> context)
    {
        await GetInnerEvents(context.HttpContext).RedirectToAccessDenied(context);
    }
 
    private CookieAuthenticationEvents GetInnerEvents(HttpContext httpContext)
    {
        if (_innerEventsType is null)
        {
            return _innerEvents;
        }
 
        return (CookieAuthenticationEvents)httpContext.RequestServices.GetRequiredService(_innerEventsType);
    }
 
    /// <summary>
    /// Marker set on <see cref="HttpContext.Items"/> during the DBSC registration cookie exchange, when
    /// the handler signs out the source scheme to drop the long-lived cookie. It tells the source
    /// scheme's sign-out hook that this is an exchange, not a user logout, so it leaves the
    /// freshly-minted session and refresh cookies intact.
    /// </summary>
    internal const string RegistrationExchangeItemKey = "__DeviceBoundSession.RegistrationExchange";
 
    /// <summary>
    /// Clears the DBSC-derived session and refresh cookies when the source scheme signs out, so a plain
    /// <c>SignOutAsync(sourceScheme)</c> fully logs the user out. The application never needs to know the
    /// derived scheme names to clear them — just as it never needs them to create them. Shared with the
    /// delegate-based sign-out wiring installed by <see cref="PostConfigureDeviceBoundSessionCookieOptions"/>.
    /// </summary>
    /// <param name="httpContext">The current HTTP context.</param>
    /// <param name="dbscScheme">The resolved DBSC handler scheme whose options name the derived schemes.</param>
    internal static async Task ClearDerivedCookiesAsync(HttpContext httpContext, string dbscScheme)
    {
        // During DBSC registration the handler signs out the source scheme to complete the cookie
        // exchange. That is not a user logout, so leave the session/refresh cookies just minted intact.
        if (httpContext.Items.ContainsKey(RegistrationExchangeItemKey))
        {
            return;
        }
 
        var options = httpContext.RequestServices
            .GetRequiredService<IOptionsMonitor<DeviceBoundSessionOptions>>()
            .Get(dbscScheme);
 
        if (options.SessionScheme is not null)
        {
            await httpContext.SignOutAsync(options.SessionScheme);
        }
 
        if (options.RefreshScheme is not null)
        {
            await httpContext.SignOutAsync(options.RefreshScheme);
        }
    }
}