File: DeviceBoundSessionOptions.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;
 
namespace Microsoft.AspNetCore.Authentication.DeviceBoundSessions;
 
/// <summary>
/// Options for the Device Bound Session Credentials authentication handler.
/// </summary>
[Experimental("ASP0031", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
public class DeviceBoundSessionOptions : AuthenticationSchemeOptions
{
    /// <summary>
    /// Gets or sets the authentication scheme used for initial sign-in (the source of the long-lived cookie).
    /// During registration, the handler authenticates against this scheme to read the user's ticket.
    /// </summary>
    public string? RegistrationSourceScheme { get; set; }
 
    /// <summary>
    /// Gets or sets the authentication scheme for the refresh cookie (path-scoped stash).
    /// During refresh, the handler authenticates against this scheme to retrieve the ticket + public key.
    /// </summary>
    public string? RefreshScheme { get; set; }
 
    /// <summary>
    /// Gets or sets the authentication scheme used to stamp the short-lived session cookie.
    /// Both registration and refresh call <c>SignInAsync</c> on this scheme.
    /// </summary>
    public string? SessionScheme { get; set; }
 
    /// <summary>
    /// Gets or sets the nonempty application-local, root-relative request path for the registration endpoint.
    /// The path must begin with <c>/</c> and is relative to the application's base path. Full URLs,
    /// current-directory-relative URL references, network-path references, query strings, and fragments are unsupported.
    /// The effective <see cref="HttpRequest.PathBase"/> is prepended when the path is advertised to the browser.
    /// Although the DBSC protocol permits absolute and cross-origin same-site endpoint references, they are outside
    /// this component's local request-handler model.
    /// Defaults to <c>/.well-known/dbsc/registration</c>.
    /// </summary>
    public PathString RegistrationPath { get; set; } = DeviceBoundSessionDefaults.RegistrationPath;
 
    /// <summary>
    /// Gets or sets the nonempty application-local, root-relative request path for the refresh endpoint.
    /// The path must begin with <c>/</c> and is relative to the application's base path. Full URLs,
    /// current-directory-relative URL references, network-path references, query strings, and fragments are unsupported.
    /// The effective <see cref="HttpRequest.PathBase"/> is prepended when the path is advertised to the browser.
    /// Although the DBSC protocol permits absolute and cross-origin same-site endpoint references, they are outside
    /// this component's local request-handler model.
    /// Defaults to <c>/.well-known/dbsc/refresh</c>.
    /// </summary>
    public PathString RefreshPath { get; set; } = DeviceBoundSessionDefaults.RefreshPath;
 
    /// <summary>
    /// Checks that the options are valid for a specific scheme.
    /// </summary>
    /// <param name="scheme">The scheme being validated.</param>
    public override void Validate(string scheme)
    {
        base.Validate(scheme);
 
        ValidatePath(RegistrationPath, nameof(RegistrationPath), scheme);
        ValidatePath(RefreshPath, nameof(RefreshPath), scheme);
 
        if (RegistrationPath.Equals(RefreshPath))
        {
            throw CreatePathCollisionException(nameof(RefreshPath), scheme);
        }
    }
 
    /// <summary>
    /// Gets or sets the expiration for the short-lived session cookie.
    /// Defaults to 10 minutes.
    /// </summary>
    public TimeSpan ShortLivedCookieExpiration { get; set; } = TimeSpan.FromMinutes(10);
 
    /// <summary>
    /// Gets or sets the maximum age for challenges before they are considered stale.
    /// Defaults to 5 minutes.
    /// </summary>
    public TimeSpan ChallengeMaxAge { get; set; } = TimeSpan.FromMinutes(5);
 
    /// <summary>
    /// Gets or sets whether the browser session is scoped to the registration site rather than the registration origin.
    /// <see langword="false"/> creates an origin-scoped browser session. <see langword="true"/> creates a
    /// site-scoped browser session and allows same-site request origins to initiate DBSC refresh by default.
    /// With the current implementation, <see langword="true"/> requires the registration endpoint to be served
    /// from the registrable-domain host because the scope origin is derived from the registration request origin.
    /// Cookie <c>Domain</c> and applicability remain independently configured on the source cookie scheme.
    /// Defaults to <see langword="false"/>.
    /// </summary>
    public bool IncludeSite { get; set; }
 
    /// <summary>
    /// Gets the list of scope specifications for the session.
    /// </summary>
    public IList<DeviceBoundSessionScopeRule> ScopeSpecifications { get; } = new List<DeviceBoundSessionScopeRule>();
 
    /// <summary>
    /// Gets the list of allowed refresh initiator host patterns.
    /// </summary>
    public IList<string> AllowedRefreshInitiators { get; } = new List<string>();
 
    private static ArgumentException CreatePathCollisionException(string parameterName, string scheme)
        => new(
            $"The {nameof(RefreshPath)} for scheme '{scheme}' must differ from {nameof(RegistrationPath)}.",
            parameterName);
 
    private static void ValidatePath(PathString path, string parameterName, string scheme)
    {
        if (!path.HasValue)
        {
            throw new ArgumentException(
                $"The {parameterName} for scheme '{scheme}' must be nonempty.",
                parameterName);
        }
 
        var value = path.Value;
        if (value.StartsWith("//", StringComparison.Ordinal))
        {
            throw new ArgumentException(
                $"The {parameterName} for scheme '{scheme}' must not be a network-path reference beginning with '//'.",
                parameterName);
        }
 
        if (value.Contains('?', StringComparison.Ordinal))
        {
            throw new ArgumentException(
                $"The {parameterName} for scheme '{scheme}' must not contain a query string ('?').",
                parameterName);
        }
 
        if (value.Contains('#', StringComparison.Ordinal))
        {
            throw new ArgumentException(
                $"The {parameterName} for scheme '{scheme}' must not contain a fragment ('#').",
                parameterName);
        }
    }
}