diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs index b83835affc..db5ea054c4 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs @@ -41,69 +41,84 @@ namespace Microsoft.AspNet.Security.Cookies protected override async Task AuthenticateCoreAsync() { - string cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); - if (string.IsNullOrWhiteSpace(cookie)) + AuthenticationTicket ticket = null; + try { - return null; - } - - AuthenticationTicket ticket = Options.TicketDataFormat.Unprotect(cookie); - - if (ticket == null) - { - _logger.WriteWarning(@"Unprotect ticket failed"); - return null; - } - - if (Options.SessionStore != null) - { - Claim claim = ticket.Identity.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); - if (claim == null) + string cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); + if (string.IsNullOrWhiteSpace(cookie)) { - _logger.WriteWarning(@"SessoinId missing"); return null; } - _sessionKey = claim.Value; - ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); + + ticket = Options.TicketDataFormat.Unprotect(cookie); + if (ticket == null) { - _logger.WriteWarning(@"Identity missing in session store"); + _logger.WriteWarning(@"Unprotect ticket failed"); return null; } - } - DateTimeOffset currentUtc = Options.SystemClock.UtcNow; - DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc; - DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc; - - if (expiresUtc != null && expiresUtc.Value < currentUtc) - { if (Options.SessionStore != null) { - await Options.SessionStore.RemoveAsync(_sessionKey); + Claim claim = ticket.Identity.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); + if (claim == null) + { + _logger.WriteWarning(@"SessoinId missing"); + return null; + } + _sessionKey = claim.Value; + ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); + if (ticket == null) + { + _logger.WriteWarning(@"Identity missing in session store"); + return null; + } } - return null; - } - if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration) - { - TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); - TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); + DateTimeOffset currentUtc = Options.SystemClock.UtcNow; + DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc; + DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc; - if (timeRemaining < timeElapsed) + if (expiresUtc != null && expiresUtc.Value < currentUtc) { - _shouldRenew = true; - _renewIssuedUtc = currentUtc; - TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); - _renewExpiresUtc = currentUtc.Add(timeSpan); + if (Options.SessionStore != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + return null; } + + if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration) + { + TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); + TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + if (timeRemaining < timeElapsed) + { + _shouldRenew = true; + _renewIssuedUtc = currentUtc; + TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + _renewExpiresUtc = currentUtc.Add(timeSpan); + } + } + + var context = new CookieValidateIdentityContext(Context, ticket, Options); + + await Options.Notifications.ValidateIdentity(context); + + return new AuthenticationTicket(context.Identity, context.Properties); + } + catch (Exception exception) + { + CookieExceptionContext exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.Authenticate, exception, ticket); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + return exceptionContext.Ticket; } - - var context = new CookieValidateIdentityContext(Context, ticket, Options); - - await Options.Notifications.ValidateIdentity(context); - - return new AuthenticationTicket(context.Identity, context.Properties); } protected override void ApplyResponseGrant() @@ -118,9 +133,14 @@ namespace Microsoft.AspNet.Security.Cookies var signout = SignOutContext; bool shouldSignout = signout != null; - if (shouldSignin || shouldSignout || _shouldRenew) + if (!(shouldSignin || shouldSignout || _shouldRenew)) + { + return; + } + + AuthenticationTicket model = await AuthenticateAsync(); + try { - AuthenticationTicket model = await AuthenticateAsync(); var cookieOptions = new CookieOptions { Domain = Options.CookieDomain, @@ -274,6 +294,16 @@ namespace Microsoft.AspNet.Security.Cookies } } } + catch (Exception exception) + { + CookieExceptionContext exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + } } private static bool IsHostRelative(string path) @@ -308,24 +338,37 @@ namespace Microsoft.AspNet.Security.Cookies loginUri = new AuthenticationProperties(ChallengeContext.Properties).RedirectUri; } - if (string.IsNullOrWhiteSpace(loginUri)) + try { - string currentUri = - Request.PathBase + - Request.Path + - Request.QueryString; + if (string.IsNullOrWhiteSpace(loginUri)) + { + string currentUri = + Request.PathBase + + Request.Path + + Request.QueryString; - loginUri = - Request.Scheme + - "://" + - Request.Host + - Request.PathBase + - Options.LoginPath + - new QueryString(Options.ReturnUrlParameter, currentUri); + loginUri = + Request.Scheme + + "://" + + Request.Host + + Request.PathBase + + Options.LoginPath + + new QueryString(Options.ReturnUrlParameter, currentUri); + } + + var redirectContext = new CookieApplyRedirectContext(Context, Options, loginUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + catch (Exception exception) + { + CookieExceptionContext exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } } - - var redirectContext = new CookieApplyRedirectContext(Context, Options, loginUri); - Options.Notifications.ApplyRedirect(redirectContext); } } } diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieAuthenticationNotifications.cs index efa30c02e2..e378a08c17 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieAuthenticationNotifications.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNet.Security.Cookies OnResponseSignedIn = context => { }; OnResponseSignOut = context => { }; OnApplyRedirect = DefaultBehavior.ApplyRedirect; + OnException = context => { }; } /// @@ -51,6 +52,11 @@ namespace Microsoft.AspNet.Security.Cookies /// public Action OnApplyRedirect { get; set; } + /// + /// A delegate assigned to this property will be invoked when the related method is called + /// + public Action OnException { get; set; } + /// /// Implements the interface method by invoking the related delegate method /// @@ -96,5 +102,14 @@ namespace Microsoft.AspNet.Security.Cookies { OnApplyRedirect.Invoke(context); } + + /// + /// Implements the interface method by invoking the related delegate method + /// + /// Contains information about the event + public virtual void Exception(CookieExceptionContext context) + { + OnException.Invoke(context); + } } } diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieExceptionContext.cs b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieExceptionContext.cs new file mode 100644 index 0000000000..e8e4fc80bb --- /dev/null +++ b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieExceptionContext.cs @@ -0,0 +1,83 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Notifications; + +namespace Microsoft.AspNet.Security.Cookies +{ + /// + /// Context object passed to the ICookieAuthenticationProvider method Exception. + /// + public class CookieExceptionContext : BaseContext + { + /// + /// Creates a new instance of the context object. + /// + /// The HTTP request context + /// The middleware options + /// The location of the exception + /// The exception thrown. + /// The current ticket, if any. + public CookieExceptionContext( + HttpContext context, + CookieAuthenticationOptions options, + ExceptionLocation location, + Exception exception, + AuthenticationTicket ticket) + : base(context, options) + { + Location = location; + Exception = exception; + Rethrow = true; + Ticket = ticket; + } + + /// + /// The code paths where exceptions may be reported. + /// + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", + Target = "Microsoft.Owin.Security.Cookies.CookieExceptionContext+ExceptionLocation", Justification = "It is a directly related option.")] + public enum ExceptionLocation + { + /// + /// The exception was reported in the Authenticate code path. + /// + Authenticate, + + /// + /// The exception was reported in the ApplyResponseGrant code path, during sign-in, sign-out, or refresh. + /// + ApplyResponseGrant, + + /// + /// The exception was reported in the ApplyResponseChallenge code path, during redirect generation. + /// + ApplyResponseChallenge, + } + + /// + /// The code path the exception occurred in. + /// + public ExceptionLocation Location { get; private set; } + + /// + /// The exception thrown. + /// + public Exception Exception { get; private set; } + + /// + /// True if the exception should be re-thrown (default), false if it should be suppressed. + /// + public bool Rethrow { get; set; } + + /// + /// The current authentication ticket, if any. + /// In the AuthenticateAsync code path, if the given exception is not re-thrown then this ticket + /// will be returned to the application. The ticket may be replaced if needed. + /// + public AuthenticationTicket Ticket { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/ICookieAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Cookies/Notifications/ICookieAuthenticationNotifications.cs index 046dcd1538..b6ced343f4 100644 --- a/src/Microsoft.AspNet.Security.Cookies/Notifications/ICookieAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Cookies/Notifications/ICookieAuthenticationNotifications.cs @@ -43,5 +43,11 @@ namespace Microsoft.AspNet.Security.Cookies /// /// Contains information about the login session as well as information about the authentication cookie. void ResponseSignOut(CookieResponseSignOutContext context); + + /// + /// Called when an exception occurs during request or response processing. + /// + /// Contains information about the exception that occurred + void Exception(CookieExceptionContext context); } }