// 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.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.Framework.Internal; using Microsoft.Framework.Logging; using Microsoft.Framework.WebEncoders; namespace Microsoft.AspNet.Authentication { /// /// Base class for the per-request work performed by most authentication middleware. /// public abstract class AuthenticationHandler : IAuthenticationHandler { private bool _finishCalled; private AuthenticationOptions _baseOptions; 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 ILogger Logger { get; private set; } protected IUrlEncoder UrlEncoder { get; private set; } internal AuthenticationOptions BaseOptions { get { return _baseOptions; } } public IAuthenticationHandler PriorHandler { get; set; } protected string CurrentUri { get { return Request.Scheme + "://" + Request.Host + Request.PathBase + Request.Path + Request.QueryString; } } protected async Task BaseInitializeAsync([NotNull] AuthenticationOptions options, [NotNull] HttpContext context, [NotNull] ILogger logger, [NotNull] IUrlEncoder encoder) { _baseOptions = options; Context = context; OriginalPathBase = Request.PathBase; Logger = logger; UrlEncoder = encoder; RegisterAuthenticationHandler(); Response.OnStarting(OnStartingCallback, this); if (BaseOptions.AutomaticAuthentication) { var ticket = await AuthenticateAsync(); if (ticket?.Principal != null) { SecurityHelper.AddUserPrincipal(Context, ticket.Principal); } } } 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 && BaseOptions.AutomaticAuthentication && Response.StatusCode == 401) { await HandleUnauthorizedAsync(new ChallengeContext(BaseOptions.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 InvokeAsync() { return Task.FromResult(false); } public void GetDescriptions(DescribeSchemesContext describeContext) { describeContext.Accept(BaseOptions.Description.Items); if (PriorHandler != null) { PriorHandler.GetDescriptions(describeContext); } } public async Task AuthenticateAsync(AuthenticateContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { var ticket = await AuthenticateAsync(); if (ticket?.Principal != null) { context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); } else { context.NotAuthenticated(); } } if (PriorHandler != null) { await PriorHandler.AuthenticateAsync(context); } } /// /// Calling Authenticate more than once should always return the original value. /// /// The ticket data provided by the authentication logic public abstract Task AuthenticateAsync(); public bool ShouldHandleScheme(string authenticationScheme) { return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || (BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme)); } public async Task SignInAsync(SignInContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { SignInAccepted = true; await HandleSignInAsync(context); context.Accept(); } 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)) { SignOutAccepted = true; await HandleSignOutAsync(context); context.Accept(); } if (PriorHandler != null) { await PriorHandler.SignOutAsync(context); } } protected virtual Task HandleSignOutAsync(SignOutContext context) { return Task.FromResult(0); } /// /// /// /// True if no other handlers should be called 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) { bool handled = false; ChallengeCalled = true; if (ShouldHandleScheme(context.AuthenticationScheme)) { switch (context.Behavior) { case ChallengeBehavior.Automatic: // If there is a principal already, invoke the forbidden code path var ticket = await AuthenticateAsync(); if (ticket?.Principal != null) { handled = await HandleForbiddenAsync(context); } else { handled = await HandleUnauthorizedAsync(context); } break; case ChallengeBehavior.Unauthorized: handled = await HandleUnauthorizedAsync(context); break; case ChallengeBehavior.Forbidden: handled = await HandleForbiddenAsync(context); 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; } } }