// Copyright (c) Microsoft Open Technologies, Inc. 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.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.DataHandler.Encoder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.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 static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); private Task _authenticate; private bool _authenticateInitialized; private object _authenticateSyncLock; private Task _applyResponse; private bool _applyResponseInitialized; private object _applyResponseSyncLock; private AuthenticationOptions _baseOptions; protected ChallengeContext ChallengeContext { get; set; } protected SignInContext SignInContext { get; set; } protected SignOutContext SignOutContext { get; set; } protected HttpContext Context { get; private set; } protected HttpRequest Request { get { return Context.Request; } } protected HttpResponse Response { get { return Context.Response; } } protected PathString RequestPathBase { get; private set; } protected ILogger Logger { get; private set; } protected IUrlEncoder UrlEncoder { get; private set; } internal AuthenticationOptions BaseOptions { get { return _baseOptions; } } internal bool AuthenticateCalled { get; set; } public IAuthenticationHandler PriorHandler { get; set; } public bool Faulted { get; set; } protected async Task BaseInitializeAsync([NotNull] AuthenticationOptions options, [NotNull] HttpContext context, [NotNull] ILogger logger, [NotNull] IUrlEncoder encoder) { _baseOptions = options; Context = context; RequestPathBase = Request.PathBase; Logger = logger; UrlEncoder = encoder; RegisterAuthenticationHandler(); Response.OnSendingHeaders(OnSendingHeaderCallback, this); await InitializeCoreAsync(); if (BaseOptions.AutomaticAuthentication) { var ticket = await AuthenticateAsync(); if (ticket?.Principal != null) { SecurityHelper.AddUserPrincipal(Context, ticket.Principal); } } } private static void OnSendingHeaderCallback(object state) { AuthenticationHandler handler = (AuthenticationHandler)state; handler.ApplyResponse(); } protected virtual Task InitializeCoreAsync() { return Task.FromResult(0); } /// /// Called once per request after Initialize and Invoke. /// /// async completion internal async Task TeardownAsync() { try { await ApplyResponseAsync(); } catch (Exception) { try { await TeardownCoreAsync(); } catch (Exception) { // Don't mask the original exception } UnregisterAuthenticationHandler(); throw; } await TeardownCoreAsync(); UnregisterAuthenticationHandler(); } protected virtual Task TeardownCoreAsync() { return Task.FromResult(0); } /// /// 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 virtual void GetDescriptions(DescribeSchemesContext describeContext) { describeContext.Accept(BaseOptions.Description.Items); if (PriorHandler != null) { PriorHandler.GetDescriptions(describeContext); } } public virtual void Authenticate(AuthenticateContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { var ticket = Authenticate(); if (ticket?.Principal != null) { AuthenticateCalled = true; context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); } else { context.NotAuthenticated(); } } if (PriorHandler != null) { PriorHandler.Authenticate(context); } } public virtual async Task AuthenticateAsync(AuthenticateContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { var ticket = await AuthenticateAsync(); if (ticket?.Principal != null) { AuthenticateCalled = true; context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); } else { context.NotAuthenticated(); } } if (PriorHandler != null) { await PriorHandler.AuthenticateAsync(context); } } public AuthenticationTicket Authenticate() { return LazyInitializer.EnsureInitialized( ref _authenticate, ref _authenticateInitialized, ref _authenticateSyncLock, () => { return Task.FromResult(AuthenticateCore()); }).GetAwaiter().GetResult(); } protected abstract AuthenticationTicket AuthenticateCore(); /// /// Causes the authentication logic in AuthenticateCore to be performed for the current request /// at most once and returns the results. Calling Authenticate more than once will always return /// the original value. /// /// This method should always be called instead of calling AuthenticateCore directly. /// /// The ticket data provided by the authentication logic public Task AuthenticateAsync() { return LazyInitializer.EnsureInitialized( ref _authenticate, ref _authenticateInitialized, ref _authenticateSyncLock, AuthenticateCoreAsync); } /// /// The core authentication logic which must be provided by the handler. Will be invoked at most /// once per request. Do not call directly, call the wrapping Authenticate method instead. /// /// The ticket data provided by the authentication logic protected virtual Task AuthenticateCoreAsync() { return Task.FromResult(AuthenticateCore()); } private void ApplyResponse() { // If ApplyResponse already failed in the OnSendingHeaderCallback or TeardownAsync code path then a // failed task is cached. If called again the same error will be re-thrown. This breaks error handling // scenarios like the ability to display the error page or re-execute the request. try { if (!Faulted) { LazyInitializer.EnsureInitialized( ref _applyResponse, ref _applyResponseInitialized, ref _applyResponseSyncLock, () => { ApplyResponseCore(); return Task.FromResult(0); }).GetAwaiter().GetResult(); // Block if the async version is in progress. } } catch (Exception) { Faulted = true; throw; } } protected virtual void ApplyResponseCore() { ApplyResponseGrant(); ApplyResponseChallenge(); } /// /// Causes the ApplyResponseCore to be invoked at most once per request. This method will be /// invoked either earlier, when the response headers are sent as a result of a response write or flush, /// or later, as the last step when the original async call to the middleware is returning. /// /// private async Task ApplyResponseAsync() { // If ApplyResponse already failed in the OnSendingHeaderCallback or TeardownAsync code path then a // failed task is cached. If called again the same error will be re-thrown. This breaks error handling // scenarios like the ability to display the error page or re-execute the request. try { if (!Faulted) { await LazyInitializer.EnsureInitialized( ref _applyResponse, ref _applyResponseInitialized, ref _applyResponseSyncLock, ApplyResponseCoreAsync); } } catch (Exception) { Faulted = true; throw; } } /// /// Core method that may be overridden by handler. The default behavior is to call two common response /// activities, one that deals with sign-in/sign-out concerns, and a second to deal with 401 challenges. /// /// protected virtual async Task ApplyResponseCoreAsync() { await ApplyResponseGrantAsync(); await ApplyResponseChallengeAsync(); } protected abstract void ApplyResponseGrant(); /// /// Override this method to dela with sign-in/sign-out concerns, if an authentication scheme in question /// deals with grant/revoke as part of it's request flow. (like setting/deleting cookies) /// /// protected virtual Task ApplyResponseGrantAsync() { ApplyResponseGrant(); return Task.FromResult(0); } public virtual void SignIn(SignInContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { SignInContext = context; SignOutContext = null; context.Accept(); } if (PriorHandler != null) { PriorHandler.SignIn(context); } } public virtual void SignOut(SignOutContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { SignInContext = null; SignOutContext = context; context.Accept(); } if (PriorHandler != null) { PriorHandler.SignOut(context); } } public virtual void Challenge(ChallengeContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) { ChallengeContext = context; context.Accept(); } if (PriorHandler != null) { PriorHandler.Challenge(context); } } protected abstract void ApplyResponseChallenge(); public virtual bool ShouldHandleScheme(string authenticationScheme) { return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || (BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme)); } public virtual bool ShouldConvertChallengeToForbidden() { // Return 403 iff 401 and this handler's authenticate was called // and the challenge is for the authentication type return Response.StatusCode == 401 && AuthenticateCalled && ChallengeContext != null && ShouldHandleScheme(ChallengeContext.AuthenticationScheme); } /// /// 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.) /// /// protected virtual Task ApplyResponseChallengeAsync() { ApplyResponseChallenge(); return Task.FromResult(0); } protected void GenerateCorrelationId([NotNull] AuthenticationProperties properties) { var correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; var nonceBytes = new byte[32]; CryptoRandom.GetBytes(nonceBytes); var correlationId = TextEncodings.Base64Url.Encode(nonceBytes); var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsHttps }; properties.Items[correlationKey] = correlationId; Response.Cookies.Append(correlationKey, correlationId, cookieOptions); } protected bool ValidateCorrelationId([NotNull] AuthenticationProperties properties) { var correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationScheme; var correlationCookie = Request.Cookies[correlationKey]; if (string.IsNullOrWhiteSpace(correlationCookie)) { Logger.LogWarning("{0} cookie not found.", correlationKey); return false; } var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsHttps }; Response.Cookies.Delete(correlationKey, cookieOptions); string correlationExtra; if (!properties.Items.TryGetValue( correlationKey, out correlationExtra)) { Logger.LogWarning("{0} state property not found.", correlationKey); return false; } properties.Items.Remove(correlationKey); if (!string.Equals(correlationCookie, correlationExtra, StringComparison.Ordinal)) { Logger.LogWarning("{0} correlation cookie and state property mismatch.", correlationKey); return false; } return true; } private void RegisterAuthenticationHandler() { var auth = Context.GetAuthentication(); PriorHandler = auth.Handler; auth.Handler = this; } private void UnregisterAuthenticationHandler() { var auth = Context.GetAuthentication(); auth.Handler = PriorHandler; } } }