using System; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Authentication { /// <summary> /// An opinionated abstraction for implementing <see cref="IAuthenticationHandler"/>. /// </summary> /// <typeparam name="TOptions">The type for the options used to configure the authentication handler.</typeparam> public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new() { private Task<AuthenticateResult>? _authenticateTask; /// <summary> /// Gets or sets the <see cref="AuthenticationScheme"/> asssociated with this authentication handler. /// </summary> public AuthenticationScheme Scheme { get; private set; } = default!; /// <summary> /// Gets or sets the options associated with this authentication handler. /// </summary> public TOptions Options { get; private set; } = default!; /// <summary> /// Gets or sets the <see cref="HttpContext"/>. /// </summary> protected HttpContext Context { get; private set; } = default!; /// <summary> /// Gets the <see cref="HttpRequest"/> associated with the current request. /// </summary> protected HttpRequest Request { get => Context.Request; } /// <summary> /// Gets the <see cref="HttpResponse" /> associated with the current request. /// </summary> protected HttpResponse Response { get => Context.Response; } /// <summary> /// Gets the path as seen by the authentication middleware. /// </summary> protected PathString OriginalPath => Context.Features.Get<IAuthenticationFeature>()?.OriginalPath ?? Request.Path; /// <summary> /// Gets the path base as seen by the authentication middleware. /// </summary> protected PathString OriginalPathBase => Context.Features.Get<IAuthenticationFeature>()?.OriginalPathBase ?? Request.PathBase; /// <summary> /// Gets the <see cref="ILogger"/>. /// </summary> protected ILogger Logger { get; } /// <summary> /// Gets the <see cref="UrlEncoder"/>. /// </summary> protected UrlEncoder UrlEncoder { get; } /// <summary> /// Gets the <see cref="ISystemClock"/>. /// </summary> protected ISystemClock Clock { get; } /// <summary> /// Gets the <see cref="IOptionsMonitor{TOptions}"/> to detect changes to options. /// </summary> protected IOptionsMonitor<TOptions> OptionsMonitor { get; } /// <summary> /// The handler calls methods on the events which give the application control at certain points where processing is occurring. /// If it is not provided a default instance is supplied which does nothing when the methods are called. /// </summary> protected virtual object? Events { get; set; } /// <summary> /// Gets the issuer that should be used when any claims are issued. /// </summary> /// <value> /// The <c>ClaimsIssuer</c> configured in <typeparamref name="TOptions"/>, if configured, otherwise <see cref="AuthenticationScheme.Name"/>. /// </value> protected virtual string ClaimsIssuer => Options.ClaimsIssuer ?? Scheme.Name; /// <summary> /// Gets the absolute current url. /// </summary> protected string CurrentUri { get => Request.Scheme + Uri.SchemeDelimiter + Request.Host + Request.PathBase + Request.Path + Request.QueryString; } /// <summary> /// Initializes a new instance of <see cref="AuthenticationHandler{TOptions}"/>. /// </summary> /// <param name="options">The monitor for the options instance.</param> /// <param name="logger">The <see cref="ILoggerFactory"/>.</param> /// <param name="encoder">The <see cref="System.Text.Encodings.Web.UrlEncoder"/>.</param> /// <param name="clock">The <see cref="ISystemClock"/>.</param> protected AuthenticationHandler(IOptionsMonitor<TOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) { Logger = logger.CreateLogger(this.GetType().FullName!); UrlEncoder = encoder; Clock = clock; OptionsMonitor = options; } /// <summary> /// Initialize the handler, resolve the options and validate them. /// </summary> /// <param name="scheme"></param> /// <param name="context"></param> /// <returns></returns> public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { if (scheme == null) { throw new ArgumentNullException(nameof(scheme)); } if (context == null) { throw new ArgumentNullException(nameof(context)); } Scheme = scheme; Context = context; Options = OptionsMonitor.Get(Scheme.Name); await InitializeEventsAsync(); await InitializeHandlerAsync(); } /// <summary> /// Initializes the events object, called once per request by <see cref="InitializeAsync(AuthenticationScheme, HttpContext)"/>. /// </summary> protected virtual async Task InitializeEventsAsync() { Events = Options.Events; if (Options.EventsType != null) { Events = Context.RequestServices.GetRequiredService(Options.EventsType); } Events ??= await CreateEventsAsync(); } /// <summary> /// Creates a new instance of the events instance. /// </summary> /// <returns>A new instance of the events instance.</returns> protected virtual Task<object> CreateEventsAsync() => Task.FromResult(new object()); /// <summary> /// Called after options/events have been initialized for the handler to finish initializing itself. /// </summary> /// <returns>A task</returns> protected virtual Task InitializeHandlerAsync() => Task.CompletedTask; /// <summary> /// Constructs an absolute url for the specified <paramref name="targetPath"/>. /// </summary> /// <param name="targetPath">The path.</param> /// <returns>The absolute url.</returns> protected string BuildRedirectUri(string targetPath) => Request.Scheme + Uri.SchemeDelimiter + Request.Host + OriginalPathBase + targetPath; /// <summary> /// Resolves the scheme that this authentication operation is forwarded to. /// </summary> /// <param name="scheme">The scheme to forward. One of ForwardAuthenticate, ForwardChallenge, ForwardForbid, ForwardSignIn, or ForwardSignOut.</param> /// <returns>The forwarded scheme or <see langword="null"/>.</returns> protected virtual string? ResolveTarget(string? scheme) { var target = scheme ?? Options.ForwardDefaultSelector?.Invoke(Context) ?? Options.ForwardDefault; // Prevent self targetting return string.Equals(target, Scheme.Name, StringComparison.Ordinal) ? null : target; } /// <inheritdoc /> public async Task<AuthenticateResult> AuthenticateAsync() { var target = ResolveTarget(Options.ForwardAuthenticate); if (target != null) { return await Context.AuthenticateAsync(target); } // Calling Authenticate more than once should always return the original value. var result = await HandleAuthenticateOnceAsync() ?? AuthenticateResult.NoResult(); if (result.Failure == null) { var ticket = result.Ticket; if (ticket?.Principal != null) { Logger.AuthenticationSchemeAuthenticated(Scheme.Name); } else { Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name); } } else { Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message); } return result; } /// <summary> /// Used to ensure HandleAuthenticateAsync is only invoked once. The subsequent calls /// will return the same authenticate result. /// </summary> protected Task<AuthenticateResult> HandleAuthenticateOnceAsync() { if (_authenticateTask == null) { _authenticateTask = HandleAuthenticateAsync(); } return _authenticateTask; } /// <summary> /// Used to ensure HandleAuthenticateAsync is only invoked once safely. The subsequent /// calls will return the same authentication result. Any exceptions will be converted /// into a failed authentication result containing the exception. /// </summary> protected async Task<AuthenticateResult> HandleAuthenticateOnceSafeAsync() { try { return await HandleAuthenticateOnceAsync(); } catch (Exception ex) { return AuthenticateResult.Fail(ex); } } /// <summary> /// Allows derived types to handle authentication. /// </summary> /// <returns>The <see cref="AuthenticateResult"/>.</returns> protected abstract Task<AuthenticateResult> HandleAuthenticateAsync(); /// <summary> /// Override this method to handle Forbid. /// </summary> /// <param name="properties"></param> /// <returns>A Task.</returns> protected virtual Task HandleForbiddenAsync(AuthenticationProperties properties) { Response.StatusCode = 403; return Task.CompletedTask; } /// <summary> /// Override this method to deal with 401 challenge concerns, if an authentication scheme in question /// deals an authentication interaction as part of it's request flow. (like adding a response header, or /// changing the 401 result to 302 of a login page or external sign-in location.) /// </summary> /// <param name="properties"></param> /// <returns>A Task.</returns> protected virtual Task HandleChallengeAsync(AuthenticationProperties properties) { Response.StatusCode = 401; return Task.CompletedTask; } /// <inheritdoc /> public async Task ChallengeAsync(AuthenticationProperties? properties) { var target = ResolveTarget(Options.ForwardChallenge); if (target != null) { await Context.ChallengeAsync(target, properties); return; } properties ??= new AuthenticationProperties(); await HandleChallengeAsync(properties); Logger.AuthenticationSchemeChallenged(Scheme.Name); } /// <inheritdoc /> public async Task ForbidAsync(AuthenticationProperties? properties) { var target = ResolveTarget(Options.ForwardForbid); if (target != null) { await Context.ForbidAsync(target, properties); return; } properties ??= new AuthenticationProperties(); await HandleForbiddenAsync(properties); Logger.AuthenticationSchemeForbidden(Scheme.Name); } } }