// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Authentication { /// /// Base class for the per-request work performed by most authentication middleware. /// /// Specifies which type for of AuthenticationOptions property public abstract class AuthenticationHandler : IAuthenticationHandler where TOptions : AuthenticationOptions { private Task _authenticateTask; private bool _finishCalled; protected bool SignInAccepted { get; set; } protected bool SignOutAccepted { get; set; } protected bool ChallengeCalled { get; set; } protected HttpContext Context { get; private set; } protected HttpRequest Request { get { return Context.Request; } } protected HttpResponse Response { get { return Context.Response; } } protected PathString OriginalPathBase { get; private set; } protected PathString OriginalPath { get; private set; } protected ILogger Logger { get; private set; } protected UrlEncoder UrlEncoder { get; private set; } public IAuthenticationHandler PriorHandler { get; set; } protected string CurrentUri { get { return Request.Scheme + "://" + Request.Host + Request.PathBase + Request.Path + Request.QueryString; } } protected TOptions Options { get; private set; } /// /// Initialize is called once per request to contextualize this instance with appropriate state. /// /// The original options passed by the application control behavior /// The utility object to observe the current request and response /// The logging factory used to create loggers /// The . /// async completion public async Task InitializeAsync(TOptions options, HttpContext context, ILogger logger, UrlEncoder encoder) { if (options == null) { throw new ArgumentNullException(nameof(options)); } if (context == null) { throw new ArgumentNullException(nameof(context)); } if (logger == null) { throw new ArgumentNullException(nameof(logger)); } if (encoder == null) { throw new ArgumentNullException(nameof(encoder)); } Options = options; Context = context; OriginalPathBase = Request.PathBase; OriginalPath = Request.Path; Logger = logger; UrlEncoder = encoder; RegisterAuthenticationHandler(); Response.OnStarting(OnStartingCallback, this); if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate)) { var result = await HandleAuthenticateOnceAsync(); if (result.Failure != null) { Logger.LogInformation(0, $"{Options.AuthenticationScheme} not authenticated: " + result.Failure.Message); } var ticket = result?.Ticket; if (ticket?.Principal != null) { Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal); Logger.LogInformation(0, "HttpContext.User merged via AutomaticAuthentication from authenticationScheme: {scheme}.", Options.AuthenticationScheme); } } } protected string BuildRedirectUri(string targetPath) { return Request.Scheme + "://" + Request.Host + OriginalPathBase + targetPath; } private static async Task OnStartingCallback(object state) { var handler = (AuthenticationHandler)state; await handler.FinishResponseOnce(); } private async Task FinishResponseOnce() { if (!_finishCalled) { _finishCalled = true; await FinishResponseAsync(); await HandleAutomaticChallengeIfNeeded(); } } /// /// Hook that is called when the response about to be sent /// /// protected virtual Task FinishResponseAsync() { return Task.FromResult(0); } private async Task HandleAutomaticChallengeIfNeeded() { if (!ChallengeCalled && Options.AutomaticChallenge && Response.StatusCode == 401) { await HandleUnauthorizedAsync(new ChallengeContext(Options.AuthenticationScheme)); } } /// /// Called once after Invoke by AuthenticationMiddleware. /// /// async completion internal async Task TeardownAsync() { try { await FinishResponseOnce(); } finally { UnregisterAuthenticationHandler(); } } /// /// Called once by common code after initialization. If an authentication middleware responds directly to /// specifically known paths it must override this virtual, compare the request path to it's known paths, /// provide any response information as appropriate, and true to stop further processing. /// /// Returning false will cause the common code to call the next middleware in line. Returning true will /// cause the common code to begin the async completion journey without calling the rest of the middleware /// pipeline. public virtual Task HandleRequestAsync() { return Task.FromResult(false); } public void GetDescriptions(DescribeSchemesContext describeContext) { describeContext.Accept(Options.Description.Items); if (PriorHandler != null) { PriorHandler.GetDescriptions(describeContext); } } public bool ShouldHandleScheme(string authenticationScheme, bool handleAutomatic) { return string.Equals(Options.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || (handleAutomatic && string.Equals(authenticationScheme, AuthenticationManager.AutomaticScheme, StringComparison.Ordinal)); } public async Task AuthenticateAsync(AuthenticateContext context) { var handled = false; if (ShouldHandleScheme(context.AuthenticationScheme, Options.AutomaticAuthenticate)) { // Calling Authenticate more than once should always return the original value. var result = await HandleAuthenticateOnceAsync(); if (result?.Failure != null) { context.Failed(result.Failure); } else { var ticket = result?.Ticket; if (ticket?.Principal != null) { context.Authenticated(ticket.Principal, ticket.Properties.Items, Options.Description.Items); Logger.LogInformation(1, "AuthenticationScheme: {scheme} was successfully authenticated.", Options.AuthenticationScheme); handled = true; } else { context.NotAuthenticated(); Logger.LogDebug(2, "AuthenticationScheme: {scheme} was not authenticated.", Options.AuthenticationScheme); } } } if (PriorHandler != null && !handled) { await PriorHandler.AuthenticateAsync(context); } } protected Task HandleAuthenticateOnceAsync() { if (_authenticateTask == null) { _authenticateTask = HandleAuthenticateAsync(); } return _authenticateTask; } protected abstract Task HandleAuthenticateAsync(); public async Task SignInAsync(SignInContext context) { if (ShouldHandleScheme(context.AuthenticationScheme, handleAutomatic: false)) { SignInAccepted = true; await HandleSignInAsync(context); Logger.LogInformation(3, "AuthenticationScheme: {scheme} signed in.", Options.AuthenticationScheme); context.Accept(); } else if (PriorHandler != null) { await PriorHandler.SignInAsync(context); } } protected virtual Task HandleSignInAsync(SignInContext context) { return Task.FromResult(0); } public async Task SignOutAsync(SignOutContext context) { if (ShouldHandleScheme(context.AuthenticationScheme, handleAutomatic: false)) { SignOutAccepted = true; await HandleSignOutAsync(context); Logger.LogInformation(4, "AuthenticationScheme: {scheme} signed out.", Options.AuthenticationScheme); context.Accept(); } else if (PriorHandler != null) { await PriorHandler.SignOutAsync(context); } } protected virtual Task HandleSignOutAsync(SignOutContext context) { return Task.FromResult(0); } protected virtual Task HandleForbiddenAsync(ChallengeContext context) { Response.StatusCode = 403; return Task.FromResult(true); } /// /// 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.) /// /// /// True if no other handlers should be called protected virtual Task HandleUnauthorizedAsync(ChallengeContext context) { Response.StatusCode = 401; return Task.FromResult(false); } public async Task ChallengeAsync(ChallengeContext context) { ChallengeCalled = true; var handled = false; if (ShouldHandleScheme(context.AuthenticationScheme, Options.AutomaticChallenge)) { switch (context.Behavior) { case ChallengeBehavior.Automatic: // If there is a principal already, invoke the forbidden code path var result = await HandleAuthenticateOnceAsync(); if (result?.Ticket?.Principal != null) { goto case ChallengeBehavior.Forbidden; } goto case ChallengeBehavior.Unauthorized; case ChallengeBehavior.Unauthorized: handled = await HandleUnauthorizedAsync(context); Logger.LogInformation(5, "AuthenticationScheme: {scheme} was challenged.", Options.AuthenticationScheme); break; case ChallengeBehavior.Forbidden: handled = await HandleForbiddenAsync(context); Logger.LogInformation(6, "AuthenticationScheme: {scheme} was forbidden.", Options.AuthenticationScheme); break; } context.Accept(); } if (!handled && PriorHandler != null) { await PriorHandler.ChallengeAsync(context); } } private void RegisterAuthenticationHandler() { var auth = Context.GetAuthentication(); PriorHandler = auth.Handler; auth.Handler = this; } private void UnregisterAuthenticationHandler() { var auth = Context.GetAuthentication(); auth.Handler = PriorHandler; } } }