diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs index d37a179d43..12c40ea5cd 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -27,75 +27,101 @@ namespace Microsoft.AspNet.Authentication.Cookies private DateTimeOffset? _renewIssuedUtc; private DateTimeOffset? _renewExpiresUtc; private string _sessionKey; + private Task _cookieTicketTask; - protected override async Task AuthenticateAsync() + private Task EnsureCookieTicket() + { + // We only need to read the ticket once + if (_cookieTicketTask == null) + { + _cookieTicketTask = ReadCookieTicket(); + } + return _cookieTicketTask; + } + + private async Task ReadCookieTicket() + { + var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); + if (string.IsNullOrEmpty(cookie)) + { + return null; + } + + var ticket = Options.TicketDataFormat.Unprotect(cookie); + if (ticket == null) + { + Logger.LogWarning(@"Unprotect ticket failed"); + return null; + } + + if (Options.SessionStore != null) + { + var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); + if (claim == null) + { + Logger.LogWarning(@"SessionId missing"); + return null; + } + _sessionKey = claim.Value; + ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); + if (ticket == null) + { + Logger.LogWarning(@"Identity missing in session store"); + return null; + } + } + + var currentUtc = Options.SystemClock.UtcNow; + var issuedUtc = ticket.Properties.IssuedUtc; + var expiresUtc = ticket.Properties.ExpiresUtc; + + if (expiresUtc != null && expiresUtc.Value < currentUtc) + { + if (Options.SessionStore != null) + { + await Options.SessionStore.RemoveAsync(_sessionKey); + } + return null; + } + + var allowRefresh = ticket.Properties.AllowRefresh ?? true; + if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + if (timeRemaining < timeElapsed) + { + _shouldRenew = true; + _renewIssuedUtc = currentUtc; + var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + _renewExpiresUtc = currentUtc.Add(timeSpan); + } + } + + // Finally we have a valid ticket + return ticket; + } + + protected override async Task HandleAuthenticateAsync() { AuthenticationTicket ticket = null; try { - var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); - if (string.IsNullOrEmpty(cookie)) - { - return null; - } - - ticket = Options.TicketDataFormat.Unprotect(cookie); - + ticket = await EnsureCookieTicket(); if (ticket == null) { - Logger.LogWarning(@"Unprotect ticket failed"); return null; } - if (Options.SessionStore != null) - { - var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); - if (claim == null) - { - Logger.LogWarning(@"SessionId missing"); - return null; - } - _sessionKey = claim.Value; - ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); - if (ticket == null) - { - Logger.LogWarning(@"Identity missing in session store"); - return null; - } - } - - var currentUtc = Options.SystemClock.UtcNow; - var issuedUtc = ticket.Properties.IssuedUtc; - var expiresUtc = ticket.Properties.ExpiresUtc; - - if (expiresUtc != null && expiresUtc.Value < currentUtc) - { - if (Options.SessionStore != null) - { - await Options.SessionStore.RemoveAsync(_sessionKey); - } - return null; - } - - var allowRefresh = ticket.Properties.AllowRefresh ?? true; - if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh) - { - var timeElapsed = currentUtc.Subtract(issuedUtc.Value); - var timeRemaining = expiresUtc.Value.Subtract(currentUtc); - - if (timeRemaining < timeElapsed) - { - _shouldRenew = true; - _renewIssuedUtc = currentUtc; - var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); - _renewExpiresUtc = currentUtc.Add(timeSpan); - } - } - var context = new CookieValidatePrincipalContext(Context, ticket, Options); - await Options.Notifications.ValidatePrincipal(context); + if (context.Principal == null) + { + return null; + } + if (context.ShouldRenew) { _shouldRenew = true; @@ -135,46 +161,6 @@ namespace Microsoft.AspNet.Authentication.Cookies return cookieOptions; } - private async Task ApplyCookie(AuthenticationTicket ticket) - { - if (_renewIssuedUtc.HasValue) - { - ticket.Properties.IssuedUtc = _renewIssuedUtc; - } - if (_renewExpiresUtc.HasValue) - { - ticket.Properties.ExpiresUtc = _renewExpiresUtc; - } - - if (Options.SessionStore != null && _sessionKey != null) - { - await Options.SessionStore.RenewAsync(_sessionKey, ticket); - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, - Options.AuthenticationScheme)); - ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); - } - - var cookieValue = Options.TicketDataFormat.Protect(ticket); - - var cookieOptions = BuildCookieOptions(); - if (ticket.Properties.IsPersistent && _renewExpiresUtc.HasValue) - { - cookieOptions.Expires = _renewExpiresUtc.Value.ToUniversalTime().DateTime; - } - - Options.CookieManager.AppendResponseCookie( - Context, - Options.CookieName, - cookieValue, - cookieOptions); - - Response.Headers.Set(HeaderNameCacheControl, HeaderValueNoCache); - Response.Headers.Set(HeaderNamePragma, HeaderValueNoCache); - Response.Headers.Set(HeaderNameExpires, HeaderValueMinusOne); - } - protected override async Task FinishResponseAsync() { // Only renew if requested, and neither sign in or sign out was called @@ -183,15 +169,48 @@ namespace Microsoft.AspNet.Authentication.Cookies return; } - var ticket = await AuthenticateOnceAsync(); + var ticket = await HandleAuthenticateOnceAsync(); try { - await ApplyCookie(ticket); + if (_renewIssuedUtc.HasValue) + { + ticket.Properties.IssuedUtc = _renewIssuedUtc; + } + if (_renewExpiresUtc.HasValue) + { + ticket.Properties.ExpiresUtc = _renewExpiresUtc; + } + + if (Options.SessionStore != null && _sessionKey != null) + { + await Options.SessionStore.RenewAsync(_sessionKey, ticket); + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, + Options.AuthenticationScheme)); + ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + } + + var cookieValue = Options.TicketDataFormat.Protect(ticket); + + var cookieOptions = BuildCookieOptions(); + if (ticket.Properties.IsPersistent && _renewExpiresUtc.HasValue) + { + cookieOptions.Expires = _renewExpiresUtc.Value.ToUniversalTime().DateTime; + } + + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + cookieOptions); + + ApplyHeaders(); } catch (Exception exception) { var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, ticket); + CookieExceptionContext.ExceptionLocation.FinishResponse, exception, ticket); Options.Notifications.Exception(exceptionContext); if (exceptionContext.Rethrow) { @@ -202,7 +221,7 @@ namespace Microsoft.AspNet.Authentication.Cookies protected override async Task HandleSignInAsync(SignInContext signin) { - var model = await AuthenticateAsync(); + var ticket = await EnsureCookieTicket(); try { var cookieOptions = BuildCookieOptions(); @@ -239,21 +258,21 @@ namespace Microsoft.AspNet.Authentication.Cookies signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; } - model = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); + ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); if (Options.SessionStore != null) { if (_sessionKey != null) { await Options.SessionStore.RemoveAsync(_sessionKey); } - _sessionKey = await Options.SessionStore.StoreAsync(model); + _sessionKey = await Options.SessionStore.StoreAsync(ticket); var principal = new ClaimsPrincipal( new ClaimsIdentity( new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, Options.ClaimsIssuer)); - model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); + ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); } - var cookieValue = Options.TicketDataFormat.Protect(model); + var cookieValue = Options.TicketDataFormat.Protect(ticket); Options.CookieManager.AppendResponseCookie( Context, @@ -270,36 +289,13 @@ namespace Microsoft.AspNet.Authentication.Cookies Options.Notifications.ResponseSignedIn(signedInContext); - Response.Headers.Set( - HeaderNameCacheControl, - HeaderValueNoCache); - - Response.Headers.Set( - HeaderNamePragma, - HeaderValueNoCache); - - Response.Headers.Set( - HeaderNameExpires, - HeaderValueMinusOne); - - var shouldLoginRedirect = Options.LoginPath.HasValue && Request.Path == Options.LoginPath; - - if ((shouldLoginRedirect) && Response.StatusCode == 200) - { - var query = Request.Query; - var redirectUri = query.Get(Options.ReturnUrlParameter); - if (!string.IsNullOrEmpty(redirectUri) - && IsHostRelative(redirectUri)) - { - var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); - Options.Notifications.ApplyRedirect(redirectContext); - } - } + var shouldLoginRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath; + ApplyHeaders(shouldLoginRedirect); } catch (Exception exception) { var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model); + CookieExceptionContext.ExceptionLocation.SignIn, exception, ticket); Options.Notifications.Exception(exceptionContext); if (exceptionContext.Rethrow) { @@ -310,11 +306,10 @@ namespace Microsoft.AspNet.Authentication.Cookies protected override async Task HandleSignOutAsync(SignOutContext signOutContext) { - var model = await AuthenticateAsync(); + var ticket = await EnsureCookieTicket(); try { var cookieOptions = BuildCookieOptions(); - if (Options.SessionStore != null && _sessionKey != null) { await Options.SessionStore.RemoveAsync(_sessionKey); @@ -332,43 +327,38 @@ namespace Microsoft.AspNet.Authentication.Cookies Options.CookieName, context.CookieOptions); - Response.Headers.Set( - HeaderNameCacheControl, - HeaderValueNoCache); - - Response.Headers.Set( - HeaderNamePragma, - HeaderValueNoCache); - - Response.Headers.Set( - HeaderNameExpires, - HeaderValueMinusOne); - - var shouldLogoutRedirect = Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; - - if (shouldLogoutRedirect && Response.StatusCode == 200) - { - var query = Request.Query; - var redirectUri = query.Get(Options.ReturnUrlParameter); - if (!string.IsNullOrEmpty(redirectUri) - && IsHostRelative(redirectUri)) - { - var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); - Options.Notifications.ApplyRedirect(redirectContext); - } - } + var shouldLogoutRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath; + ApplyHeaders(shouldLogoutRedirect); } catch (Exception exception) { var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model); + CookieExceptionContext.ExceptionLocation.SignOut, exception, ticket); Options.Notifications.Exception(exceptionContext); if (exceptionContext.Rethrow) { throw; } } + } + private void ApplyHeaders(bool shouldRedirectToReturnUrl = false) + { + Response.Headers.Set(HeaderNameCacheControl, HeaderValueNoCache); + Response.Headers.Set(HeaderNamePragma, HeaderValueNoCache); + Response.Headers.Set(HeaderNameExpires, HeaderValueMinusOne); + + if (shouldRedirectToReturnUrl && Response.StatusCode == 200) + { + var query = Request.Query; + var redirectUri = query.Get(Options.ReturnUrlParameter); + if (!string.IsNullOrEmpty(redirectUri) + && IsHostRelative(redirectUri)) + { + var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + } } private static bool IsHostRelative(string path) @@ -387,36 +377,34 @@ namespace Microsoft.AspNet.Authentication.Cookies protected override Task HandleForbiddenAsync(ChallengeContext context) { // HandleForbidden by redirecting to AccessDeniedPath if set - if (Options.AccessDeniedPath.HasValue) - { - try - { - var accessDeniedUri = - Request.Scheme + - "://" + - Request.Host + - Request.PathBase + - Options.AccessDeniedPath; - - var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri); - Options.Notifications.ApplyRedirect(redirectContext); - } - catch (Exception exception) - { - var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); - Options.Notifications.Exception(exceptionContext); - if (exceptionContext.Rethrow) - { - throw; - } - } - return Task.FromResult(true); - } - else + if (!Options.AccessDeniedPath.HasValue) { return base.HandleForbiddenAsync(context); } + + try + { + var accessDeniedUri = + Request.Scheme + + "://" + + Request.Host + + OriginalPathBase + + Options.AccessDeniedPath; + + var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri); + Options.Notifications.ApplyRedirect(redirectContext); + } + catch (Exception exception) + { + var exceptionContext = new CookieExceptionContext(Context, Options, + CookieExceptionContext.ExceptionLocation.Forbidden, exception, ticket: null); + Options.Notifications.Exception(exceptionContext); + if (exceptionContext.Rethrow) + { + throw; + } + } + return Task.FromResult(true); } protected override Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) @@ -431,27 +419,17 @@ namespace Microsoft.AspNet.Authentication.Cookies { if (string.IsNullOrEmpty(redirectUri)) { - redirectUri = - Request.PathBase + - Request.Path + - Request.QueryString; + redirectUri = OriginalPathBase + Request.Path + Request.QueryString; } - var loginUri = - Request.Scheme + - "://" + - Request.Host + - Request.PathBase + - Options.LoginPath + - QueryString.Create(Options.ReturnUrlParameter, redirectUri); - - var redirectContext = new CookieApplyRedirectContext(Context, Options, loginUri); + var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri); + var redirectContext = new CookieApplyRedirectContext(Context, Options, BuildRedirectUri(loginUri)); Options.Notifications.ApplyRedirect(redirectContext); } catch (Exception exception) { var exceptionContext = new CookieExceptionContext(Context, Options, - CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); + CookieExceptionContext.ExceptionLocation.Unauthorized, exception, ticket: null); Options.Notifications.Exception(exceptionContext); if (exceptionContext.Rethrow) { diff --git a/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieExceptionContext.cs b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieExceptionContext.cs index 0d51f95d44..1b66997ec3 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieExceptionContext.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/Notifications/CookieExceptionContext.cs @@ -48,14 +48,30 @@ namespace Microsoft.AspNet.Authentication.Cookies Authenticate, /// - /// The exception was reported in the ApplyResponseGrant code path, during sign-in, sign-out, or refresh. + /// The exception was reported in the FinishResponse code path, during sign-in, sign-out, or refresh. /// - ApplyResponseGrant, + FinishResponse, /// - /// The exception was reported in the ApplyResponseChallenge code path, during redirect generation. + /// The exception was reported in the Unauthorized code path, during redirect generation. /// - ApplyResponseChallenge, + Unauthorized, + + /// + /// The exception was reported in the Forbidden code path, during redirect generation. + /// + Forbidden, + + /// + /// The exception was reported in the SignIn code path + /// + SignIn, + + /// + /// The exception was reported in the SignOut code path + /// + SignOut, + } /// diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs index 64dd51bc4c..480c3da04b 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs @@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Authentication.OAuth public async Task InvokeReturnPathAsync() { - var ticket = await AuthenticateAsync(); + var ticket = await HandleAuthenticateOnceAsync(); if (ticket == null) { Logger.LogWarning("Invalid return state, unable to redirect."); @@ -78,7 +78,7 @@ namespace Microsoft.AspNet.Authentication.OAuth return context.IsRequestCompleted; } - protected override async Task AuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { AuthenticationProperties properties = null; try diff --git a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs index fef3d857df..a163ec5378 100644 --- a/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuthBearer/OAuthBearerAuthenticationHandler.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using set in the options. /// /// - protected override async Task AuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { string token = null; try diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs index b3203bb8ab..3fa4097e99 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectAuthenticationHandler.cs @@ -224,7 +224,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect /// /// An if successful. /// Uses log id's OIDCH-0000 - OIDCH-0025 - protected override async Task AuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType()); @@ -903,7 +903,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect private async Task InvokeReplyPathAsync() { - var ticket = await AuthenticateAsync(); + var ticket = await HandleAuthenticateOnceAsync(); if (ticket != null) { if (ticket.Principal != null) diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs index 51229df9f8..19df41b908 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs @@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Authentication.Twitter return false; } - protected override async Task AuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { AuthenticationProperties properties = null; try @@ -169,7 +169,7 @@ namespace Microsoft.AspNet.Authentication.Twitter public async Task InvokeReturnPathAsync() { - var model = await AuthenticateAsync(); + var model = await HandleAuthenticateOnceAsync(); if (model == null) { Logger.LogWarning("Invalid return state, unable to redirect."); diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs index 7dff2c0944..623ac82f9c 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs @@ -38,6 +38,8 @@ namespace Microsoft.AspNet.Authentication protected PathString OriginalPathBase { get; private set; } + protected PathString OriginalPath { get; private set; } + protected ILogger Logger { get; private set; } protected IUrlEncoder UrlEncoder { get; private set; } @@ -62,6 +64,7 @@ namespace Microsoft.AspNet.Authentication _baseOptions = options; Context = context; OriginalPathBase = Request.PathBase; + OriginalPath = Request.Path; Logger = logger; UrlEncoder = encoder; @@ -72,7 +75,7 @@ namespace Microsoft.AspNet.Authentication // Automatic authentication is the empty scheme if (ShouldHandleScheme(string.Empty)) { - var ticket = await AuthenticateOnceAsync(); + var ticket = await HandleAuthenticateOnceAsync(); if (ticket?.Principal != null) { Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal); @@ -157,13 +160,10 @@ namespace Microsoft.AspNet.Authentication } } - protected Task AuthenticateOnceAsync() + public bool ShouldHandleScheme(string authenticationScheme) { - if (_authenticateTask == null) - { - _authenticateTask = AuthenticateAsync(); - } - return _authenticateTask; + return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || + (BaseOptions.AutomaticAuthentication && string.IsNullOrEmpty(authenticationScheme)); } public async Task AuthenticateAsync(AuthenticateContext context) @@ -171,11 +171,10 @@ namespace Microsoft.AspNet.Authentication if (ShouldHandleScheme(context.AuthenticationScheme)) { // Calling Authenticate more than once should always return the original value. - var ticket = await AuthenticateOnceAsync(); + var ticket = await HandleAuthenticateOnceAsync(); if (ticket?.Principal != null) { context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); - _authenticateTask = Task.FromResult(ticket); } else { @@ -189,14 +188,17 @@ namespace Microsoft.AspNet.Authentication } } - protected abstract Task AuthenticateAsync(); - - public bool ShouldHandleScheme(string authenticationScheme) + protected Task HandleAuthenticateOnceAsync() { - return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || - (BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme)); + if (_authenticateTask == null) + { + _authenticateTask = HandleAuthenticateAsync(); + } + return _authenticateTask; } + protected abstract Task HandleAuthenticateAsync(); + public async Task SignInAsync(SignInContext context) { if (ShouldHandleScheme(context.AuthenticationScheme)) @@ -270,7 +272,7 @@ namespace Microsoft.AspNet.Authentication { case ChallengeBehavior.Automatic: // If there is a principal already, invoke the forbidden code path - var ticket = await AuthenticateAsync(); + var ticket = await HandleAuthenticateOnceAsync(); if (ticket?.Principal != null) { handled = await HandleForbiddenAsync(context); diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/TicketSerializer.cs b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/TicketSerializer.cs index 360692e0e6..01d232a9cc 100644 --- a/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/TicketSerializer.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/Serializer/TicketSerializer.cs @@ -43,8 +43,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Serializer var principal = model.Principal; if (principal == null) { - // Use -1 to signal null - writer.Write(-1); + throw new ArgumentNullException("model.Principal"); } else { @@ -75,37 +74,36 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Serializer { return null; } - string authenticationScheme = reader.ReadString(); - int identityCount = reader.ReadInt32(); - ClaimsPrincipal principal = null; + var authenticationScheme = reader.ReadString(); + var identityCount = reader.ReadInt32(); - // Negative values are used to signify null - if (identityCount >= 0) + if (identityCount < 0) { - var identities = new ClaimsIdentity[identityCount]; - for (int i = 0; i != identityCount; ++i) + return null; + } + + var identities = new ClaimsIdentity[identityCount]; + for (var i = 0; i != identityCount; ++i) + { + var authenticationType = reader.ReadString(); + var nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType); + var roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType); + var count = reader.ReadInt32(); + var claims = new Claim[count]; + for (int index = 0; index != count; ++index) { - string authenticationType = reader.ReadString(); - string nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType); - string roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType); - int count = reader.ReadInt32(); - var claims = new Claim[count]; - for (int index = 0; index != count; ++index) - { - string type = ReadWithDefault(reader, nameClaimType); - string value = reader.ReadString(); - string valueType = ReadWithDefault(reader, DefaultValues.StringValueType); - string issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority); - string originalIssuer = ReadWithDefault(reader, issuer); - claims[index] = new Claim(type, value, valueType, issuer, originalIssuer); - } - identities[i] = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType); + var type = ReadWithDefault(reader, nameClaimType); + var value = reader.ReadString(); + var valueType = ReadWithDefault(reader, DefaultValues.StringValueType); + var issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority); + var originalIssuer = ReadWithDefault(reader, issuer); + claims[index] = new Claim(type, value, valueType, issuer, originalIssuer); } - principal = new ClaimsPrincipal(identities); + identities[i] = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType); } var properties = PropertiesSerializer.Read(reader); - return new AuthenticationTicket(principal, properties, authenticationScheme); + return new AuthenticationTicket(new ClaimsPrincipal(identities), properties, authenticationScheme); } private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) @@ -122,7 +120,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Serializer private static string ReadWithDefault(BinaryReader reader, string defaultValue) { - string value = reader.ReadString(); + var value = reader.ReadString(); if (string.Equals(value, DefaultValues.DefaultStringPlaceholder, StringComparison.Ordinal)) { return defaultValue; diff --git a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs index adb5bccb62..2a3a9044f1 100644 --- a/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs +++ b/test/Microsoft.AspNet.Authentication.Test/AuthenticationHandlerFacts.cs @@ -32,6 +32,20 @@ namespace Microsoft.AspNet.Authentication Assert.True(handler.ShouldHandleScheme("")); } + [Fact] + public void AutomaticHandlerHandlesNullScheme() + { + var handler = new TestAutoHandler("ignored", true); + Assert.True(handler.ShouldHandleScheme(null)); + } + + [Fact] + public void AutomaticHandlerIgnoresWhitespaceScheme() + { + var handler = new TestAutoHandler("ignored", true); + Assert.False(handler.ShouldHandleScheme(" ")); + } + [Fact] public void AutomaticHandlerShouldHandleSchemeWhenSchemeMatches() { @@ -53,6 +67,37 @@ namespace Microsoft.AspNet.Authentication Assert.False(handler.ShouldHandleScheme("Alpha")); } + [Theory] + [InlineData("Alpha")] + [InlineData("")] + public async Task AuthHandlerAuthenticateCachesTicket(string scheme) + { + var handler = new CountHandler(scheme); + var context = new AuthenticateContext(scheme); + await handler.AuthenticateAsync(context); + await handler.AuthenticateAsync(context); + Assert.Equal(1, handler.AuthCount); + } + + private class CountHandler : AuthenticationHandler + { + public int AuthCount = 0; + + public CountHandler(string scheme) + { + Initialize(new TestOptions(), new DefaultHttpContext(), new LoggerFactory().CreateLogger("TestHandler"), Framework.WebEncoders.UrlEncoder.Default); + Options.AuthenticationScheme = scheme; + Options.AutomaticAuthentication = true; + } + + protected override Task HandleAuthenticateAsync() + { + AuthCount++; + return Task.FromResult(new AuthenticationTicket(null, null)); + } + + } + private class TestHandler : AuthenticationHandler { public TestHandler(string scheme) @@ -61,7 +106,7 @@ namespace Microsoft.AspNet.Authentication Options.AuthenticationScheme = scheme; } - protected override Task AuthenticateAsync() + protected override Task HandleAuthenticateAsync() { throw new NotImplementedException(); } @@ -86,7 +131,7 @@ namespace Microsoft.AspNet.Authentication Options.AutomaticAuthentication = auto; } - protected override Task AuthenticateAsync() + protected override Task HandleAuthenticateAsync() { throw new NotImplementedException(); } diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs index 42de3e8ad9..de511a994d 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -371,6 +371,36 @@ namespace Microsoft.AspNet.Authentication.Cookies FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe(null); } + [Fact] + public async Task CookieCanBeRejectedAndSignedOutByValidator() + { + var clock = new TestClock(); + var server = CreateServer(options => + { + options.SystemClock = clock; + options.ExpireTimeSpan = TimeSpan.FromMinutes(10); + options.SlidingExpiration = false; + options.Notifications = new CookieAuthenticationNotifications + { + OnValidatePrincipal = ctx => + { + ctx.RejectPrincipal(); + ctx.HttpContext.Authentication.SignOutAsync("Cookies"); + return Task.FromResult(0); + } + }; + }, + context => + context.Authentication.SignInAsync("Cookies", + new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); + + var transaction1 = await SendAsync(server, "http://example.com/testpath"); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); + transaction2.SetCookie.ShouldContain(".AspNet.Cookies=; expires="); + FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe(null); + } + [Fact] public async Task CookieCanBeRenewedByValidator() { @@ -671,6 +701,149 @@ namespace Microsoft.AspNet.Authentication.Cookies transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); } + [Fact] + public async Task MapWillNotAffectChallenge() + { + var server = TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.LoginPath = new PathString("/page")); + app.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); + }, + services => services.AddAuthentication()); + + var transaction = await server.SendAsync("http://example.com/login"); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + + var location = transaction.Response.Headers.Location; + location.LocalPath.ShouldBe("/page"); + location.Query.ShouldBe("?ReturnUrl=%2F"); + } + + [Fact] + public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath() + { + var server = TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.LoginPath = new PathString("/login")); + app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.Authentication.SignInAsync("Cookies", + new ClaimsPrincipal()))); + }, + services => services.AddAuthentication()); + + var transaction = await server.SendAsync("http://example.com/notlogin?ReturnUrl=%2Fpage"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + transaction.SetCookie.ShouldNotBe(null); + } + + [Fact] + public async Task MapWillNotAffectSignInRedirectToReturnUrl() + { + var server = TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.LoginPath = new PathString("/login")); + app.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.SignInAsync("Cookies", + new ClaimsPrincipal()))); + }, + services => services.AddAuthentication()); + + var transaction = await server.SendAsync("http://example.com/login?ReturnUrl=%2Fpage"); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.SetCookie.ShouldNotBe(null); + + var location = transaction.Response.Headers.Location; + location.OriginalString.ShouldBe("/page"); + } + + [Fact] + public async Task MapWithSignOutOnlyRedirectToReturnUrlOnLogoutPath() + { + var server = TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.LogoutPath = new PathString("/logout")); + app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.Authentication.SignOutAsync("Cookies"))); + }, + services => services.AddAuthentication()); + + var transaction = await server.SendAsync("http://example.com/notlogout?ReturnUrl=%2Fpage"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + transaction.SetCookie[0].ShouldContain(".AspNet.Cookies=; expires="); + } + + [Fact] + public async Task MapWillNotAffectSignOutRedirectToReturnUrl() + { + var server = TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.LogoutPath = new PathString("/logout")); + app.Map("/logout", signoutApp => signoutApp.Run(context => context.Authentication.SignOutAsync("Cookies"))); + }, + services => services.AddAuthentication()); + + var transaction = await server.SendAsync("http://example.com/logout?ReturnUrl=%2Fpage"); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.SetCookie[0].ShouldContain(".AspNet.Cookies=; expires="); + + var location = transaction.Response.Headers.Location; + location.OriginalString.ShouldBe("/page"); + } + + [Fact] + public async Task MapWillNotAffectAccessDenied() + { + var server = TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.AccessDeniedPath = new PathString("/denied")); + app.Map("/forbid", signoutApp => signoutApp.Run(context => context.Authentication.ForbidAsync("Cookies"))); + }, + services => services.AddAuthentication()); + var transaction = await server.SendAsync("http://example.com/forbid"); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + + var location = transaction.Response.Headers.Location; + location.LocalPath.ShouldBe("/denied"); + } + + [Fact] + public async Task NestedMapWillNotAffectLogin() + { + var server = TestServer.Create(app => + app.Map("/base", map => + { + map.UseCookieAuthentication(options => options.LoginPath = new PathString("/page")); + map.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); + }), + services => services.AddAuthentication()); + var transaction = await server.SendAsync("http://example.com/base/login"); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + + var location = transaction.Response.Headers.Location; + location.LocalPath.ShouldBe("/base/page"); + location.Query.ShouldBe("?ReturnUrl=%2F"); + } + + [Fact] + public async Task NestedMapWillNotAffectAccessDenied() + { + var server = TestServer.Create(app => + app.Map("/base", map => + { + map.UseCookieAuthentication(options => options.AccessDeniedPath = new PathString("/denied")); + map.Map("/forbid", signoutApp => signoutApp.Run(context => context.Authentication.ForbidAsync("Cookies"))); + }), + services => services.AddAuthentication()); + var transaction = await server.SendAsync("http://example.com/base/forbid"); + + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + + var location = transaction.Response.Headers.Location; + location.LocalPath.ShouldBe("/base/denied"); + } + private static string FindClaimValue(Transaction transaction, string claimType) { var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); diff --git a/test/Microsoft.AspNet.Authentication.Test/DataHandler/Encoder/TicketSerializerTests.cs b/test/Microsoft.AspNet.Authentication.Test/DataHandler/Encoder/TicketSerializerTests.cs index bf3dbaec5e..2e9978d0bd 100644 --- a/test/Microsoft.AspNet.Authentication.Test/DataHandler/Encoder/TicketSerializerTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/DataHandler/Encoder/TicketSerializerTests.cs @@ -1,6 +1,7 @@ // 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.IO; using System.Linq; using System.Security.Claims; @@ -14,7 +15,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Encoder public class TicketSerializerTests { [Fact] - public void CanRoundTripNullPrincipal() + public void NullPrincipalThrows() { var properties = new AuthenticationProperties(); properties.RedirectUri = "bye"; @@ -24,12 +25,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Encoder using (var writer = new BinaryWriter(stream)) using (var reader = new BinaryReader(stream)) { - TicketSerializer.Write(writer, ticket); - stream.Position = 0; - var readTicket = TicketSerializer.Read(reader); - readTicket.Principal.ShouldBe(null); - readTicket.Properties.RedirectUri.ShouldBe("bye"); - readTicket.AuthenticationScheme.ShouldBe("Hello"); + Assert.Throws(() => TicketSerializer.Write(writer, ticket)); } }