// 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.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.HttpFeature.Security; using Microsoft.AspNet.Security.DataHandler.Encoder; using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Security.Infrastructure { /// /// Base class for the per-request work performed by most authentication middleware. /// public abstract class AuthenticationHandler : IAuthenticationHandler { private static readonly RNGCryptoServiceProvider CryptoRandom = new RNGCryptoServiceProvider(); private Task _authenticate; private bool _authenticateInitialized; private object _authenticateSyncLock; private Task _applyResponse; private bool _applyResponseInitialized; private object _applyResponseSyncLock; private AuthenticationOptions _baseOptions; protected IChallengeContext ChallengeContext { get; set; } protected SignInIdentityContext SignInIdentityContext { get; set; } protected ISignOutContext 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; } internal AuthenticationOptions BaseOptions { get { return _baseOptions; } } public IAuthenticationHandler PriorHandler { get; set; } protected async Task BaseInitializeAsync(AuthenticationOptions options, HttpContext context) { _baseOptions = options; Context = context; RequestPathBase = Request.PathBase; RegisterAuthenticationHandler(); Response.OnSendingHeaders(OnSendingHeaderCallback, this); await InitializeCoreAsync(); if (BaseOptions.AuthenticationMode == AuthenticationMode.Active) { AuthenticationTicket ticket = await AuthenticateAsync(); if (ticket != null && ticket.Identity != null) { SecurityHelper.AddUserIdentity(Context, ticket.Identity); } } } 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() { await ApplyResponseAsync(); 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(IAuthTypeContext authTypeContext) { authTypeContext.Accept(BaseOptions.Description.Dictionary); if (PriorHandler != null) { PriorHandler.GetDescriptions(authTypeContext); } } public virtual void Authenticate(IAuthenticateContext context) { if (context.AuthenticationTypes.Contains(BaseOptions.AuthenticationType, StringComparer.Ordinal)) { AuthenticationTicket ticket = Authenticate(); if (ticket != null && ticket.Identity != null) { context.Authenticated(ticket.Identity, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); } } if (PriorHandler != null) { PriorHandler.Authenticate(context); } } public AuthenticationTicket Authenticate() { return LazyInitializer.EnsureInitialized( ref _authenticate, ref _authenticateInitialized, ref _authenticateSyncLock, () => { return Task.FromResult(AuthenticateCore()); }).Result; } protected abstract AuthenticationTicket AuthenticateCore(); public virtual async Task AuthenticateAsync(IAuthenticateContext context) { if (context.AuthenticationTypes.Contains(BaseOptions.AuthenticationType, StringComparer.Ordinal)) { AuthenticationTicket ticket = await AuthenticateAsync(); if (ticket != null && ticket.Identity != null) { context.Authenticated(ticket.Identity, ticket.Properties.Dictionary, BaseOptions.Description.Dictionary); } } if (PriorHandler != null) { await PriorHandler.AuthenticateAsync(context); } } /// /// 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() { LazyInitializer.EnsureInitialized( ref _applyResponse, ref _applyResponseInitialized, ref _applyResponseSyncLock, () => { ApplyResponseCore(); return Task.FromResult(0); }).Wait(); // Block if the async version is in progress. } 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 Task ApplyResponseAsync() { return LazyInitializer.EnsureInitialized( ref _applyResponse, ref _applyResponseInitialized, ref _applyResponseSyncLock, ApplyResponseCoreAsync); } /// /// 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(ISignInContext context) { ClaimsIdentity identity; if (SecurityHelper.LookupSignIn(context.Identities, BaseOptions.AuthenticationType, out identity)) { SignInIdentityContext = new SignInIdentityContext(identity, new AuthenticationProperties(context.Properties)); SignOutContext = null; context.Accept(BaseOptions.AuthenticationType, BaseOptions.Description.Dictionary); } if (PriorHandler != null) { PriorHandler.SignIn(context); } } public virtual void SignOut(ISignOutContext context) { if (SecurityHelper.LookupSignOut(context.AuthenticationTypes, BaseOptions.AuthenticationType, BaseOptions.AuthenticationMode)) { SignInIdentityContext = null; SignOutContext = context; context.Accept(BaseOptions.AuthenticationType, BaseOptions.Description.Dictionary); } if (PriorHandler != null) { PriorHandler.SignOut(context); } } public virtual void Challenge(IChallengeContext context) { if (SecurityHelper.LookupChallenge(context.AuthenticationTypes, BaseOptions.AuthenticationType, BaseOptions.AuthenticationMode)) { ChallengeContext = context; context.Accept(BaseOptions.AuthenticationType, BaseOptions.Description.Dictionary); } if (PriorHandler != null) { PriorHandler.Challenge(context); } } protected abstract void ApplyResponseChallenge(); /// /// 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) { string correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationType; var nonceBytes = new byte[32]; CryptoRandom.GetBytes(nonceBytes); string correlationId = TextEncodings.Base64Url.Encode(nonceBytes); var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsSecure }; properties.Dictionary[correlationKey] = correlationId; Response.Cookies.Append(correlationKey, correlationId, cookieOptions); } [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Owin.Logging.LoggerExtensions.WriteWarning(Microsoft.Owin.Logging.ILogger,System.String,System.String[])", Justification = "Logging is not Localized")] protected bool ValidateCorrelationId([NotNull] AuthenticationProperties properties, [NotNull] ILogger logger) { string correlationKey = Constants.CorrelationPrefix + BaseOptions.AuthenticationType; string correlationCookie = Request.Cookies[correlationKey]; if (string.IsNullOrWhiteSpace(correlationCookie)) { logger.WriteWarning("{0} cookie not found.", correlationKey); return false; } Response.Cookies.Delete(correlationKey); string correlationExtra; if (!properties.Dictionary.TryGetValue( correlationKey, out correlationExtra)) { logger.WriteWarning("{0} state property not found.", correlationKey); return false; } properties.Dictionary.Remove(correlationKey); if (!string.Equals(correlationCookie, correlationExtra, StringComparison.Ordinal)) { logger.WriteWarning("{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; } } }