// 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;
}
}
}