From 0623f3b741f1e98c48a4fe5cf88b77c1d3133a77 Mon Sep 17 00:00:00 2001 From: Chris R Date: Wed, 9 Dec 2015 10:52:30 -0800 Subject: [PATCH] #555 Make SkipToNextMiddleware work on events. --- samples/JwtBearerSample/Startup.cs | 2 + .../Properties/launchSettings.json | 2 +- samples/SocialSample/Startup.cs | 10 +- .../CookieAuthenticationHandler.cs | 44 +++-- .../Events/JwtBearerEvents.cs | 8 +- .../JwtBearerHandler.cs | 34 +++- .../OAuthHandler.cs | 20 +- .../OpenIdConnectHandler.cs | 32 ++-- .../TwitterHandler.cs | 8 +- .../AuthenticateResult.cs | 24 ++- .../AuthenticationHandler.cs | 8 +- .../{ErrorContext.cs => FailureContext.cs} | 11 +- .../Events/IRemoteAuthenticationEvents.cs | 2 +- .../Events/RemoteAuthenticationEvents.cs | 8 +- .../RemoteAuthenticationHandler.cs | 14 +- .../Google/GoogleMiddlewareTests.cs | 24 +-- .../JwtBearer/JwtBearerMiddlewareTests.cs | 172 +++++++++++++++++- 17 files changed, 307 insertions(+), 116 deletions(-) rename src/Microsoft.AspNet.Authentication/Events/{ErrorContext.cs => FailureContext.cs} (60%) diff --git a/samples/JwtBearerSample/Startup.cs b/samples/JwtBearerSample/Startup.cs index 5d2bd6400f..c79cbda951 100644 --- a/samples/JwtBearerSample/Startup.cs +++ b/samples/JwtBearerSample/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; using Newtonsoft.Json.Linq; namespace JwtBearerSample @@ -91,6 +92,7 @@ namespace JwtBearerSample else { response.ContentType = "application/json"; + response.Headers[HeaderNames.CacheControl] = "no-cache"; var json = JToken.FromObject(Todos); await response.WriteAsync(json.ToString()); } diff --git a/samples/OpenIdConnectSample/Properties/launchSettings.json b/samples/OpenIdConnectSample/Properties/launchSettings.json index 174032b7bc..3d9d32eebe 100644 --- a/samples/OpenIdConnectSample/Properties/launchSettings.json +++ b/samples/OpenIdConnectSample/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:1791/", + "applicationUrl": "http://localhost:42023", "sslPort": 0 } }, diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index 972f3ccc0f..01d26fcdf3 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -85,10 +85,10 @@ namespace CookieSample options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"; options.Events = new OAuthEvents() { - OnRemoteError = ctx => + OnRemoteFailure = ctx => { - ctx.Response.Redirect("/error?ErrorMessage=" + UrlEncoder.Default.Encode(ctx.Error.Message)); + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); ctx.HandleResponse(); return Task.FromResult(0); } @@ -103,9 +103,9 @@ namespace CookieSample options.ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"; options.Events = new TwitterEvents() { - OnRemoteError = ctx => + OnRemoteFailure = ctx => { - ctx.Response.Redirect("/error?ErrorMessage=" + UrlEncoder.Default.Encode(ctx.Error.Message)); + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); ctx.HandleResponse(); return Task.FromResult(0); } @@ -269,7 +269,7 @@ namespace CookieSample { context.Response.ContentType = "text/html"; await context.Response.WriteAsync(""); - await context.Response.WriteAsync("An remote error has occured: " + context.Request.Query["ErrorMessage"] + "
"); + await context.Response.WriteAsync("An remote failure has occurred: " + context.Request.Query["FailureMessage"] + "
"); await context.Response.WriteAsync("Home"); await context.Response.WriteAsync(""); }); diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs index ad7dc7e862..e65640feaf 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationHandler.cs @@ -10,7 +10,6 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features.Authentication; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -26,31 +25,30 @@ namespace Microsoft.AspNet.Authentication.Cookies private DateTimeOffset? _renewIssuedUtc; private DateTimeOffset? _renewExpiresUtc; private string _sessionKey; - private Task _cookieTicketTask; + private Task _readCookieTask; - private Task EnsureCookieTicket() + private Task EnsureCookieTicket() { // We only need to read the ticket once - if (_cookieTicketTask == null) + if (_readCookieTask == null) { - _cookieTicketTask = ReadCookieTicket(); + _readCookieTask = ReadCookieTicket(); } - return _cookieTicketTask; + return _readCookieTask; } - private async Task ReadCookieTicket() + private async Task ReadCookieTicket() { var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); if (string.IsNullOrEmpty(cookie)) { - return null; + return AuthenticateResult.Skip(); } var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding()); if (ticket == null) { - Logger.LogWarning(@"Unprotect ticket failed"); - return null; + return AuthenticateResult.Fail("Unprotect ticket failed"); } if (Options.SessionStore != null) @@ -58,15 +56,13 @@ namespace Microsoft.AspNet.Authentication.Cookies var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); if (claim == null) { - Logger.LogWarning(@"SessionId missing"); - return null; + return AuthenticateResult.Fail("SessionId missing"); } _sessionKey = claim.Value; ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); if (ticket == null) { - Logger.LogWarning(@"Identity missing in session store"); - return null; + return AuthenticateResult.Fail("Identity missing in session store"); } } @@ -80,7 +76,7 @@ namespace Microsoft.AspNet.Authentication.Cookies { await Options.SessionStore.RemoveAsync(_sessionKey); } - return null; + return AuthenticateResult.Fail("Ticket expired"); } var allowRefresh = ticket.Properties.AllowRefresh ?? true; @@ -99,23 +95,23 @@ namespace Microsoft.AspNet.Authentication.Cookies } // Finally we have a valid ticket - return ticket; + return AuthenticateResult.Success(ticket); } protected override async Task HandleAuthenticateAsync() { - var ticket = await EnsureCookieTicket(); - if (ticket == null) + var result = await EnsureCookieTicket(); + if (!result.Succeeded) { - return AuthenticateResult.Failed("No ticket."); + return result; } - var context = new CookieValidatePrincipalContext(Context, ticket, Options); + var context = new CookieValidatePrincipalContext(Context, result.Ticket, Options); await Options.Events.ValidatePrincipal(context); if (context.Principal == null) { - return AuthenticateResult.Failed("No principal."); + return AuthenticateResult.Fail("No principal."); } if (context.ShouldRenew) @@ -196,7 +192,8 @@ namespace Microsoft.AspNet.Authentication.Cookies protected override async Task HandleSignInAsync(SignInContext signin) { - var ticket = await EnsureCookieTicket(); + // Process the request cookie to initialize members like _sessionKey. + var result = await EnsureCookieTicket(); var cookieOptions = BuildCookieOptions(); var signInContext = new CookieSigningInContext( @@ -231,7 +228,7 @@ namespace Microsoft.AspNet.Authentication.Cookies signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; } - ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); + var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme); if (Options.SessionStore != null) { if (_sessionKey != null) @@ -269,6 +266,7 @@ namespace Microsoft.AspNet.Authentication.Cookies protected override async Task HandleSignOutAsync(SignOutContext signOutContext) { + // Process the request cookie to initialize members like _sessionKey. var ticket = await EnsureCookieTicket(); var cookieOptions = BuildCookieOptions(); if (Options.SessionStore != null && _sessionKey != null) diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerEvents.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerEvents.cs index e9832862b2..7fcf922e57 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerEvents.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/Events/JwtBearerEvents.cs @@ -36,13 +36,9 @@ namespace Microsoft.AspNet.Authentication.JwtBearer public Func OnValidatedToken { get; set; } = context => Task.FromResult(0); /// - /// Invoked to apply a challenge sent back to the caller. + /// Invoked before a challenge is sent back to the caller. /// - public Func OnChallenge { get; set; } = context => - { - context.HttpContext.Response.Headers.Append("WWW-Authenticate", context.Options.Challenge); - return Task.FromResult(0); - }; + public Func OnChallenge { get; set; } = context => Task.FromResult(0); public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); diff --git a/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs index beb77486f4..645cf2cdef 100644 --- a/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs +++ b/src/Microsoft.AspNet.Authentication.JwtBearer/JwtBearerHandler.cs @@ -7,10 +7,12 @@ using System.IdentityModel.Tokens; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Authentication.JwtBearer { @@ -38,7 +40,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer } if (receivingTokenContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } // If application retrieved token from somewhere else, use that. @@ -51,7 +53,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer // If no authorization header found, nothing to process further if (string.IsNullOrEmpty(authorization)) { - return AuthenticateResult.Failed("No authorization header."); + return AuthenticateResult.Skip(); } if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) @@ -62,7 +64,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer // If no token found, no further work possible if (string.IsNullOrEmpty(token)) { - return AuthenticateResult.Failed("No bearer token."); + return AuthenticateResult.Skip(); } } @@ -79,7 +81,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer } if (receivedTokenContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } if (_configuration == null && Options.ConfigurationManager != null) @@ -147,7 +149,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer } if (validatedTokenContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } return AuthenticateResult.Success(ticket); @@ -168,13 +170,13 @@ namespace Microsoft.AspNet.Authentication.JwtBearer } if (authenticationFailedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } - return AuthenticateResult.Failed(authenticationFailedContext.Exception); + return AuthenticateResult.Fail(authenticationFailedContext.Exception); } - return AuthenticateResult.Failed("No SecurityTokenValidator available for token: " + token ?? "[null]"); + return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]"); } catch (Exception ex) { @@ -192,7 +194,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer } if (authenticationFailedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } throw; @@ -201,8 +203,20 @@ namespace Microsoft.AspNet.Authentication.JwtBearer protected override async Task HandleUnauthorizedAsync(ChallengeContext context) { + var eventContext = new JwtBearerChallengeContext(Context, Options); + await Options.Events.Challenge(eventContext); + if (eventContext.HandledResponse) + { + return true; + } + if (eventContext.Skipped) + { + return false; + } + Response.StatusCode = 401; - await Options.Events.Challenge(new JwtBearerChallengeContext(Context, Options)); + Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge); + return false; } diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs index 1d9c95d3cd..1d36e1437d 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthHandler.cs @@ -38,20 +38,20 @@ namespace Microsoft.AspNet.Authentication.OAuth var error = query["error"]; if (!StringValues.IsNullOrEmpty(error)) { - var errorMessage = new StringBuilder(); - errorMessage.Append(error); + var failureMessage = new StringBuilder(); + failureMessage.Append(error); var errorDescription = query["error_description"]; if (!StringValues.IsNullOrEmpty(errorDescription)) { - errorMessage.Append(";Description=").Append(errorDescription); + failureMessage.Append(";Description=").Append(errorDescription); } var errorUri = query["error_uri"]; if (!StringValues.IsNullOrEmpty(errorUri)) { - errorMessage.Append(";Uri=").Append(errorUri); + failureMessage.Append(";Uri=").Append(errorUri); } - return AuthenticateResult.Failed(errorMessage.ToString()); + return AuthenticateResult.Fail(failureMessage.ToString()); } var code = query["code"]; @@ -60,30 +60,30 @@ namespace Microsoft.AspNet.Authentication.OAuth properties = Options.StateDataFormat.Unprotect(state); if (properties == null) { - return AuthenticateResult.Failed("The oauth state was missing or invalid."); + return AuthenticateResult.Fail("The oauth state was missing or invalid."); } // OAuth2 10.12 CSRF if (!ValidateCorrelationId(properties)) { - return AuthenticateResult.Failed("Correlation failed."); + return AuthenticateResult.Fail("Correlation failed."); } if (StringValues.IsNullOrEmpty(code)) { - return AuthenticateResult.Failed("Code was not found."); + return AuthenticateResult.Fail("Code was not found."); } var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath)); if (tokens.Error != null) { - return AuthenticateResult.Failed(tokens.Error); + return AuthenticateResult.Fail(tokens.Error); } if (string.IsNullOrEmpty(tokens.AccessToken)) { - return AuthenticateResult.Failed("Failed to retrieve access token."); + return AuthenticateResult.Fail("Failed to retrieve access token."); } var identity = new ClaimsIdentity(Options.ClaimsIssuer); diff --git a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index 5780bccfe7..124d8f543a 100644 --- a/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -307,7 +307,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security if (!string.IsNullOrEmpty(message.IdToken) || !string.IsNullOrEmpty(message.AccessToken)) { - return AuthenticateResult.Failed("An OpenID Connect response cannot contain an " + + return AuthenticateResult.Fail("An OpenID Connect response cannot contain an " + "identity token or an access token when using response_mode=query"); } } @@ -324,7 +324,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect if (message == null) { - return AuthenticateResult.Failed("No message."); + return AuthenticateResult.Fail("No message."); } try @@ -336,7 +336,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (messageReceivedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } message = messageReceivedContext.ProtocolMessage; @@ -345,7 +345,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect { // This wasn't a valid ODIC message, it may not have been intended for us. Logger.LogDebug(11, "message.State is null or empty."); - return AuthenticateResult.Failed(Resources.MessageStateIsNullOrEmpty); + return AuthenticateResult.Fail(Resources.MessageStateIsNullOrEmpty); } // if state exists and we failed to 'unprotect' this is not a message we should process. @@ -353,14 +353,14 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect if (properties == null) { Logger.LogError(12, "Unable to unprotect the message.State."); - return AuthenticateResult.Failed(Resources.MessageStateIsInvalid); + return AuthenticateResult.Fail(Resources.MessageStateIsInvalid); } // if any of the error fields are set, throw error null if (!string.IsNullOrEmpty(message.Error)) { Logger.LogError(13, "Message contains error: '{0}', error_description: '{1}', error_uri: '{2}'.", message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null"); - return AuthenticateResult.Failed(new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null"))); + return AuthenticateResult.Fail(new OpenIdConnectProtocolException(string.Format(CultureInfo.InvariantCulture, Resources.MessageContainsError, message.Error, message.ErrorDescription ?? "ErrorDecription null", message.ErrorUri ?? "ErrorUri null"))); } string userstate = null; @@ -369,7 +369,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect if (!ValidateCorrelationId(properties)) { - return AuthenticateResult.Failed("Correlation failed."); + return AuthenticateResult.Fail("Correlation failed."); } if (_configuration == null && Options.ConfigurationManager != null) @@ -393,7 +393,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect else if (authorizationResponseReceivedContext.Skipped) { Logger.LogDebug(17, "AuthorizationResponseReceived.Skipped"); - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } message = authorizationResponseReceivedContext.ProtocolMessage; properties = authorizationResponseReceivedContext.Properties; @@ -409,7 +409,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect else { Logger.LogTrace(18, "Cannot process the message. Both id_token and code are missing."); - return AuthenticateResult.Failed(Resources.IdTokenCodeMissing); + return AuthenticateResult.Fail(Resources.IdTokenCodeMissing); } } catch (Exception exception) @@ -433,7 +433,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (authenticationFailedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } throw; @@ -459,7 +459,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (authorizationCodeReceivedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } message = authorizationCodeReceivedContext.ProtocolMessage; var code = authorizationCodeReceivedContext.Code; @@ -476,7 +476,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (authorizationCodeRedeemedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } message = authorizationCodeRedeemedContext.ProtocolMessage; @@ -509,7 +509,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (authenticationValidatedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } ticket = authenticationValidatedContext.AuthenticationTicket; @@ -558,7 +558,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (authenticationValidatedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } message = authenticationValidatedContext.ProtocolMessage; ticket = authenticationValidatedContext.AuthenticationTicket; @@ -573,7 +573,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (authorizationCodeReceivedContext.Skipped) { - return AuthenticateResult.Success(ticket: null); + return AuthenticateResult.Skip(); } message = authorizationCodeReceivedContext.ProtocolMessage; ticket = authorizationCodeReceivedContext.AuthenticationTicket; @@ -671,7 +671,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect } else if (userInformationReceivedContext.Skipped) { - return null; + return ticket; } ticket = userInformationReceivedContext.AuthenticationTicket; user = userInformationReceivedContext.User; diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs index 312c8c281e..0552ed2d76 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterHandler.cs @@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Authentication.Twitter if (requestToken == null) { - return AuthenticateResult.Failed("Invalid state cookie."); + return AuthenticateResult.Fail("Invalid state cookie."); } properties = requestToken.Properties; @@ -54,18 +54,18 @@ namespace Microsoft.AspNet.Authentication.Twitter var returnedToken = query["oauth_token"]; if (StringValues.IsNullOrEmpty(returnedToken)) { - return AuthenticateResult.Failed("Missing oauth_token"); + return AuthenticateResult.Fail("Missing oauth_token"); } if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal)) { - return AuthenticateResult.Failed("Unmatched token"); + return AuthenticateResult.Fail("Unmatched token"); } var oauthVerifier = query["oauth_verifier"]; if (StringValues.IsNullOrEmpty(oauthVerifier)) { - return AuthenticateResult.Failed("Missing or blank oauth_verifier"); + return AuthenticateResult.Fail("Missing or blank oauth_verifier"); } var cookieOptions = new CookieOptions diff --git a/src/Microsoft.AspNet.Authentication/AuthenticateResult.cs b/src/Microsoft.AspNet.Authentication/AuthenticateResult.cs index 4f733fe7fd..25d87a4633 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticateResult.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticateResult.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Security.Claims; -using Microsoft.AspNet.Http.Authentication; namespace Microsoft.AspNet.Authentication { @@ -31,9 +29,14 @@ namespace Microsoft.AspNet.Authentication public AuthenticationTicket Ticket { get; private set; } /// - /// Holds error information caused by authentication. + /// Holds failure information from the authentication. /// - public Exception Error { get; private set; } + public Exception Failure { get; private set; } + + /// + /// Indicates that this stage of authentication was skipped by user intervention. + /// + public bool Skipped { get; private set; } public static AuthenticateResult Success(AuthenticationTicket ticket) { @@ -44,14 +47,19 @@ namespace Microsoft.AspNet.Authentication return new AuthenticateResult() { Ticket = ticket }; } - public static AuthenticateResult Failed(Exception error) + public static AuthenticateResult Skip() { - return new AuthenticateResult() { Error = error }; + return new AuthenticateResult() { Skipped = true }; } - public static AuthenticateResult Failed(string errorMessage) + public static AuthenticateResult Fail(Exception failure) { - return new AuthenticateResult() { Error = new Exception(errorMessage) }; + return new AuthenticateResult() { Failure = failure }; + } + + public static AuthenticateResult Fail(string failureMessage) + { + return new AuthenticateResult() { Failure = new Exception(failureMessage) }; } } diff --git a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs index 6dd298fbc7..9beeb24823 100644 --- a/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/AuthenticationHandler.cs @@ -100,6 +100,10 @@ namespace Microsoft.AspNet.Authentication if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate)) { var result = await HandleAuthenticateOnceAsync(); + if (result.Failure != null) + { + Logger.LogInformation(0, $"{Options.AuthenticationScheme} not authenticated: " + result.Failure.Message); + } var ticket = result?.Ticket; if (ticket?.Principal != null) { @@ -200,9 +204,9 @@ namespace Microsoft.AspNet.Authentication // Calling Authenticate more than once should always return the original value. var result = await HandleAuthenticateOnceAsync(); - if (result?.Error != null) + if (result?.Failure != null) { - context.Failed(result.Error); + context.Failed(result.Failure); } else { diff --git a/src/Microsoft.AspNet.Authentication/Events/ErrorContext.cs b/src/Microsoft.AspNet.Authentication/Events/FailureContext.cs similarity index 60% rename from src/Microsoft.AspNet.Authentication/Events/ErrorContext.cs rename to src/Microsoft.AspNet.Authentication/Events/FailureContext.cs index a8ef4b5944..e0475d7363 100644 --- a/src/Microsoft.AspNet.Authentication/Events/ErrorContext.cs +++ b/src/Microsoft.AspNet.Authentication/Events/FailureContext.cs @@ -2,25 +2,24 @@ // 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; namespace Microsoft.AspNet.Authentication { /// - /// Provides error context information to middleware providers. + /// Provides failure context information to middleware providers. /// - public class ErrorContext : BaseControlContext + public class FailureContext : BaseControlContext { - public ErrorContext(HttpContext context, Exception error) + public FailureContext(HttpContext context, Exception failure) : base(context) { - Error = error; + Failure = failure; } /// /// User friendly error message for the error. /// - public Exception Error { get; set; } + public Exception Failure { get; set; } } } diff --git a/src/Microsoft.AspNet.Authentication/Events/IRemoteAuthenticationEvents.cs b/src/Microsoft.AspNet.Authentication/Events/IRemoteAuthenticationEvents.cs index e19fd10b28..666783fd9c 100644 --- a/src/Microsoft.AspNet.Authentication/Events/IRemoteAuthenticationEvents.cs +++ b/src/Microsoft.AspNet.Authentication/Events/IRemoteAuthenticationEvents.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication /// /// Invoked when the remote authentication process has an error. /// - Task RemoteError(ErrorContext context); + Task RemoteFailure(FailureContext context); /// /// Invoked before sign in. diff --git a/src/Microsoft.AspNet.Authentication/Events/RemoteAuthenticationEvents.cs b/src/Microsoft.AspNet.Authentication/Events/RemoteAuthenticationEvents.cs index fce53b9927..d1c90be2f0 100644 --- a/src/Microsoft.AspNet.Authentication/Events/RemoteAuthenticationEvents.cs +++ b/src/Microsoft.AspNet.Authentication/Events/RemoteAuthenticationEvents.cs @@ -8,17 +8,17 @@ namespace Microsoft.AspNet.Authentication { public class RemoteAuthenticationEvents : IRemoteAuthenticationEvents { - public Func OnRemoteError { get; set; } = context => Task.FromResult(0); + public Func OnRemoteFailure { get; set; } = context => Task.FromResult(0); public Func OnTicketReceived { get; set; } = context => Task.FromResult(0); /// - /// Invoked when there is a remote error + /// Invoked when there is a remote failure /// - public virtual Task RemoteError(ErrorContext context) => OnRemoteError(context); + public virtual Task RemoteFailure(FailureContext context) => OnRemoteFailure(context); /// - /// Invoked after the remote ticket has been recieved. + /// Invoked after the remote ticket has been received. /// public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context); } diff --git a/src/Microsoft.AspNet.Authentication/RemoteAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication/RemoteAuthenticationHandler.cs index 8c58d072dd..dda9063697 100644 --- a/src/Microsoft.AspNet.Authentication/RemoteAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication/RemoteAuthenticationHandler.cs @@ -22,11 +22,15 @@ namespace Microsoft.AspNet.Authentication protected virtual async Task HandleRemoteCallbackAsync() { var authResult = await HandleRemoteAuthenticateAsync(); + if (authResult != null && authResult.Skipped) + { + return false; + } if (authResult == null || !authResult.Succeeded) { - var errorContext = new ErrorContext(Context, authResult?.Error ?? new Exception("Invalid return state, unable to redirect.")); - Logger.LogInformation("Error from RemoteAuthentication: " + errorContext.Error.Message); - await Options.Events.RemoteError(errorContext); + var errorContext = new FailureContext(Context, authResult?.Failure ?? new Exception("Invalid return state, unable to redirect.")); + Logger.LogInformation("Error from RemoteAuthentication: " + errorContext.Failure.Message); + await Options.Events.RemoteFailure(errorContext); if (errorContext.HandledResponse) { return true; @@ -36,7 +40,7 @@ namespace Microsoft.AspNet.Authentication return false; } - throw new AggregateException("Unhandled remote error.", errorContext.Error); + throw new AggregateException("Unhandled remote failure.", errorContext.Failure); } // We have a ticket if we get here @@ -77,7 +81,7 @@ namespace Microsoft.AspNet.Authentication protected override Task HandleAuthenticateAsync() { - return Task.FromResult(AuthenticateResult.Failed("Remote authentication does not support authenticate")); + return Task.FromResult(AuthenticateResult.Fail("Remote authentication does not support authenticate")); } protected override Task HandleSignOutAsync(SignOutContext context) diff --git a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs index 2e3336e057..77fc762840 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -261,9 +261,9 @@ namespace Microsoft.AspNet.Authentication.Google { options.Events = new OAuthEvents() { - OnRemoteError = ctx => + OnRemoteFailure = ctx => { - ctx.Response.Redirect("/error?ErrorMessage=" + UrlEncoder.Default.Encode(ctx.Error.Message)); + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); ctx.HandleResponse(); return Task.FromResult(0); } @@ -275,7 +275,7 @@ namespace Microsoft.AspNet.Authentication.Google { var transaction = await sendTask; Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?ErrorMessage=OMG"+UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First()); + Assert.Equal("/error?FailureMessage=OMG"+UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First()); } else { @@ -389,9 +389,9 @@ namespace Microsoft.AspNet.Authentication.Google { options.Events = new OAuthEvents() { - OnRemoteError = ctx => + OnRemoteFailure = ctx => { - ctx.Response.Redirect("/error?ErrorMessage=" + UrlEncoder.Default.Encode(ctx.Error.Message)); + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); ctx.HandleResponse(); return Task.FromResult(0); } @@ -412,7 +412,7 @@ namespace Microsoft.AspNet.Authentication.Google { var transaction = await sendTask; Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"), + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"), transaction.Response.Headers.GetValues("Location").First()); } else @@ -444,9 +444,9 @@ namespace Microsoft.AspNet.Authentication.Google { options.Events = new OAuthEvents() { - OnRemoteError = ctx => + OnRemoteFailure = ctx => { - ctx.Response.Redirect("/error?ErrorMessage=" + UrlEncoder.Default.Encode(ctx.Error.Message)); + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); ctx.HandleResponse(); return Task.FromResult(0); } @@ -466,7 +466,7 @@ namespace Microsoft.AspNet.Authentication.Google { var transaction = await sendTask; Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."), + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."), transaction.Response.Headers.GetValues("Location").First()); } else @@ -737,9 +737,9 @@ namespace Microsoft.AspNet.Authentication.Google options.ClientSecret = "Test Secret"; options.Events = new OAuthEvents() { - OnRemoteError = ctx => + OnRemoteFailure = ctx => { - ctx.Response.Redirect("/error?ErrorMessage=" + UrlEncoder.Default.Encode(ctx.Error.Message)); + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); ctx.HandleResponse(); return Task.FromResult(0); } @@ -751,7 +751,7 @@ namespace Microsoft.AspNet.Authentication.Google "https://example.com/signin-google?code=TestCode"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.Encode("The oauth state was missing or invalid."), + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("The oauth state was missing or invalid."), transaction.Response.Headers.GetValues("Location").First()); } diff --git a/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs index aaef7ffa91..6100957cac 100644 --- a/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs @@ -11,6 +11,7 @@ using System.Xml.Linq; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.TestHost; using Microsoft.AspNet.Testing.xunit; using Microsoft.Extensions.DependencyInjection; @@ -312,6 +313,163 @@ namespace Microsoft.AspNet.Authentication.JwtBearer Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); } + [Fact] + public async Task EventOnReceivingTokenSkipped_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.AutomaticAuthenticate = true; + + options.Events = new JwtBearerEvents() + { + OnReceivingToken = context => + { + context.SkipToNextMiddleware(); + return Task.FromResult(0); + }, + OnReceivedToken = context => + { + throw new NotImplementedException(); + }, + OnValidatedToken = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnReceivedTokenSkipped_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.AutomaticAuthenticate = true; + + options.Events = new JwtBearerEvents() + { + OnReceivedToken = context => + { + context.SkipToNextMiddleware(); + return Task.FromResult(0); + }, + OnValidatedToken = context => + { + throw new NotImplementedException(); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnValidatedTokenSkipped_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.AutomaticAuthenticate = true; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + options.Events = new JwtBearerEvents() + { + OnValidatedToken = context => + { + context.SkipToNextMiddleware(); + return Task.FromResult(0); + }, + OnAuthenticationFailed = context => + { + throw new NotImplementedException(context.Exception.ToString()); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnAuthenticationFailedSkipped_NoMoreEventsExecuted() + { + var server = CreateServer(options => + { + options.AutomaticAuthenticate = true; + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT")); + options.Events = new JwtBearerEvents() + { + OnValidatedToken = context => + { + throw new Exception("Test Exception"); + }, + OnAuthenticationFailed = context => + { + context.SkipToNextMiddleware(); + return Task.FromResult(0); + }, + OnChallenge = context => + { + throw new NotImplementedException(); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Equal(string.Empty, response.ResponseText); + } + + [Fact] + public async Task EventOnChallengeSkipped_ResponseNotModified() + { + var server = CreateServer(options => + { + options.AutomaticAuthenticate = true; + options.AutomaticChallenge = true; + options.Events = new JwtBearerEvents() + { + OnChallenge = context => + { + context.SkipToNextMiddleware(); + return Task.FromResult(0); + }, + }; + }); + + var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token"); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.Empty(response.Response.Headers.WwwAuthenticate); + Assert.Equal(string.Empty, response.ResponseText); + } + class InvalidTokenValidator : ISecurityTokenValidator { public InvalidTokenValidator() @@ -387,7 +545,17 @@ namespace Microsoft.AspNet.Authentication.JwtBearer app.Use(async (context, next) => { - if (context.Request.Path == new PathString("/oauth")) + if (context.Request.Path == new PathString("/checkforerrors")) + { + var authContext = new AuthenticateContext(Http.Authentication.AuthenticationManager.AutomaticScheme); + await context.Authentication.AuthenticateAsync(authContext); + if (authContext.Error != null) + { + throw new Exception("Failed to authenticate", authContext.Error); + } + return; + } + else if (context.Request.Path == new PathString("/oauth")) { if (context.User == null || context.User.Identity == null || @@ -408,14 +576,12 @@ namespace Microsoft.AspNet.Authentication.JwtBearer await context.Response.WriteAsync(identifier.Value); } - else if (context.Request.Path == new PathString("/unauthorized")) { // Simulate Authorization failure var result = await context.Authentication.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); await context.Authentication.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); } - else if (context.Request.Path == new PathString("/signIn")) { await Assert.ThrowsAsync(() => context.Authentication.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal()));