#555 Make SkipToNextMiddleware work on events.

This commit is contained in:
Chris R 2015-12-09 10:52:30 -08:00
parent 4c1943b281
commit 0623f3b741
17 changed files with 307 additions and 116 deletions

View File

@ -5,6 +5,7 @@ using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace JwtBearerSample namespace JwtBearerSample
@ -91,6 +92,7 @@ namespace JwtBearerSample
else else
{ {
response.ContentType = "application/json"; response.ContentType = "application/json";
response.Headers[HeaderNames.CacheControl] = "no-cache";
var json = JToken.FromObject(Todos); var json = JToken.FromObject(Todos);
await response.WriteAsync(json.ToString()); await response.WriteAsync(json.ToString());
} }

View File

@ -3,7 +3,7 @@
"windowsAuthentication": false, "windowsAuthentication": false,
"anonymousAuthentication": true, "anonymousAuthentication": true,
"iisExpress": { "iisExpress": {
"applicationUrl": "http://localhost:1791/", "applicationUrl": "http://localhost:42023",
"sslPort": 0 "sslPort": 0
} }
}, },

View File

@ -85,10 +85,10 @@ namespace CookieSample
options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"; options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f";
options.Events = new OAuthEvents() 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(); ctx.HandleResponse();
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -103,9 +103,9 @@ namespace CookieSample
options.ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"; options.ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI";
options.Events = new TwitterEvents() 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(); ctx.HandleResponse();
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -269,7 +269,7 @@ namespace CookieSample
{ {
context.Response.ContentType = "text/html"; context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<html><body>"); await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("An remote error has occured: " + context.Request.Query["ErrorMessage"] + "<br>"); await context.Response.WriteAsync("An remote failure has occurred: " + context.Request.Query["FailureMessage"] + "<br>");
await context.Response.WriteAsync("<a href=\"/\">Home</a>"); await context.Response.WriteAsync("<a href=\"/\">Home</a>");
await context.Response.WriteAsync("</body></html>"); await context.Response.WriteAsync("</body></html>");
}); });

View File

@ -10,7 +10,6 @@ using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.Http.Features.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -26,31 +25,30 @@ namespace Microsoft.AspNet.Authentication.Cookies
private DateTimeOffset? _renewIssuedUtc; private DateTimeOffset? _renewIssuedUtc;
private DateTimeOffset? _renewExpiresUtc; private DateTimeOffset? _renewExpiresUtc;
private string _sessionKey; private string _sessionKey;
private Task<AuthenticationTicket> _cookieTicketTask; private Task<AuthenticateResult> _readCookieTask;
private Task<AuthenticationTicket> EnsureCookieTicket() private Task<AuthenticateResult> EnsureCookieTicket()
{ {
// We only need to read the ticket once // 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<AuthenticationTicket> ReadCookieTicket() private async Task<AuthenticateResult> ReadCookieTicket()
{ {
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
if (string.IsNullOrEmpty(cookie)) if (string.IsNullOrEmpty(cookie))
{ {
return null; return AuthenticateResult.Skip();
} }
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding()); var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
if (ticket == null) if (ticket == null)
{ {
Logger.LogWarning(@"Unprotect ticket failed"); return AuthenticateResult.Fail("Unprotect ticket failed");
return null;
} }
if (Options.SessionStore != null) if (Options.SessionStore != null)
@ -58,15 +56,13 @@ namespace Microsoft.AspNet.Authentication.Cookies
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim)); var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null) if (claim == null)
{ {
Logger.LogWarning(@"SessionId missing"); return AuthenticateResult.Fail("SessionId missing");
return null;
} }
_sessionKey = claim.Value; _sessionKey = claim.Value;
ticket = await Options.SessionStore.RetrieveAsync(_sessionKey); ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
if (ticket == null) if (ticket == null)
{ {
Logger.LogWarning(@"Identity missing in session store"); return AuthenticateResult.Fail("Identity missing in session store");
return null;
} }
} }
@ -80,7 +76,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
{ {
await Options.SessionStore.RemoveAsync(_sessionKey); await Options.SessionStore.RemoveAsync(_sessionKey);
} }
return null; return AuthenticateResult.Fail("Ticket expired");
} }
var allowRefresh = ticket.Properties.AllowRefresh ?? true; var allowRefresh = ticket.Properties.AllowRefresh ?? true;
@ -99,23 +95,23 @@ namespace Microsoft.AspNet.Authentication.Cookies
} }
// Finally we have a valid ticket // Finally we have a valid ticket
return ticket; return AuthenticateResult.Success(ticket);
} }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{ {
var ticket = await EnsureCookieTicket(); var result = await EnsureCookieTicket();
if (ticket == null) 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); await Options.Events.ValidatePrincipal(context);
if (context.Principal == null) if (context.Principal == null)
{ {
return AuthenticateResult.Failed("No principal."); return AuthenticateResult.Fail("No principal.");
} }
if (context.ShouldRenew) if (context.ShouldRenew)
@ -196,7 +192,8 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override async Task HandleSignInAsync(SignInContext signin) 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 cookieOptions = BuildCookieOptions();
var signInContext = new CookieSigningInContext( var signInContext = new CookieSigningInContext(
@ -231,7 +228,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; 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 (Options.SessionStore != null)
{ {
if (_sessionKey != null) if (_sessionKey != null)
@ -269,6 +266,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override async Task HandleSignOutAsync(SignOutContext signOutContext) protected override async Task HandleSignOutAsync(SignOutContext signOutContext)
{ {
// Process the request cookie to initialize members like _sessionKey.
var ticket = await EnsureCookieTicket(); var ticket = await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions(); var cookieOptions = BuildCookieOptions();
if (Options.SessionStore != null && _sessionKey != null) if (Options.SessionStore != null && _sessionKey != null)

View File

@ -36,13 +36,9 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
public Func<ValidatedTokenContext, Task> OnValidatedToken { get; set; } = context => Task.FromResult(0); public Func<ValidatedTokenContext, Task> OnValidatedToken { get; set; } = context => Task.FromResult(0);
/// <summary> /// <summary>
/// Invoked to apply a challenge sent back to the caller. /// Invoked before a challenge is sent back to the caller.
/// </summary> /// </summary>
public Func<JwtBearerChallengeContext, Task> OnChallenge { get; set; } = context => public Func<JwtBearerChallengeContext, Task> OnChallenge { get; set; } = context => Task.FromResult(0);
{
context.HttpContext.Response.Headers.Append("WWW-Authenticate", context.Options.Challenge);
return Task.FromResult(0);
};
public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context); public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);

View File

@ -7,10 +7,12 @@ using System.IdentityModel.Tokens;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Http.Features.Authentication; using Microsoft.AspNet.Http.Features.Authentication;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Authentication.JwtBearer namespace Microsoft.AspNet.Authentication.JwtBearer
{ {
@ -38,7 +40,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
} }
if (receivingTokenContext.Skipped) if (receivingTokenContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
// If application retrieved token from somewhere else, use that. // 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 no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization)) if (string.IsNullOrEmpty(authorization))
{ {
return AuthenticateResult.Failed("No authorization header."); return AuthenticateResult.Skip();
} }
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
@ -62,7 +64,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
// If no token found, no further work possible // If no token found, no further work possible
if (string.IsNullOrEmpty(token)) 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) if (receivedTokenContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
if (_configuration == null && Options.ConfigurationManager != null) if (_configuration == null && Options.ConfigurationManager != null)
@ -147,7 +149,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
} }
if (validatedTokenContext.Skipped) if (validatedTokenContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
return AuthenticateResult.Success(ticket); return AuthenticateResult.Success(ticket);
@ -168,13 +170,13 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
} }
if (authenticationFailedContext.Skipped) 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) catch (Exception ex)
{ {
@ -192,7 +194,7 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
} }
if (authenticationFailedContext.Skipped) if (authenticationFailedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
throw; throw;
@ -201,8 +203,20 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
protected override async Task<bool> HandleUnauthorizedAsync(ChallengeContext context) protected override async Task<bool> 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; Response.StatusCode = 401;
await Options.Events.Challenge(new JwtBearerChallengeContext(Context, Options)); Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
return false; return false;
} }

View File

@ -38,20 +38,20 @@ namespace Microsoft.AspNet.Authentication.OAuth
var error = query["error"]; var error = query["error"];
if (!StringValues.IsNullOrEmpty(error)) if (!StringValues.IsNullOrEmpty(error))
{ {
var errorMessage = new StringBuilder(); var failureMessage = new StringBuilder();
errorMessage.Append(error); failureMessage.Append(error);
var errorDescription = query["error_description"]; var errorDescription = query["error_description"];
if (!StringValues.IsNullOrEmpty(errorDescription)) if (!StringValues.IsNullOrEmpty(errorDescription))
{ {
errorMessage.Append(";Description=").Append(errorDescription); failureMessage.Append(";Description=").Append(errorDescription);
} }
var errorUri = query["error_uri"]; var errorUri = query["error_uri"];
if (!StringValues.IsNullOrEmpty(errorUri)) 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"]; var code = query["code"];
@ -60,30 +60,30 @@ namespace Microsoft.AspNet.Authentication.OAuth
properties = Options.StateDataFormat.Unprotect(state); properties = Options.StateDataFormat.Unprotect(state);
if (properties == null) 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 // OAuth2 10.12 CSRF
if (!ValidateCorrelationId(properties)) if (!ValidateCorrelationId(properties))
{ {
return AuthenticateResult.Failed("Correlation failed."); return AuthenticateResult.Fail("Correlation failed.");
} }
if (StringValues.IsNullOrEmpty(code)) 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)); var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath));
if (tokens.Error != null) if (tokens.Error != null)
{ {
return AuthenticateResult.Failed(tokens.Error); return AuthenticateResult.Fail(tokens.Error);
} }
if (string.IsNullOrEmpty(tokens.AccessToken)) 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); var identity = new ClaimsIdentity(Options.ClaimsIssuer);

View File

@ -307,7 +307,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
if (!string.IsNullOrEmpty(message.IdToken) || !string.IsNullOrEmpty(message.AccessToken)) 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"); "identity token or an access token when using response_mode=query");
} }
} }
@ -324,7 +324,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
if (message == null) if (message == null)
{ {
return AuthenticateResult.Failed("No message."); return AuthenticateResult.Fail("No message.");
} }
try try
@ -336,7 +336,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (messageReceivedContext.Skipped) else if (messageReceivedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
message = messageReceivedContext.ProtocolMessage; 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. // This wasn't a valid ODIC message, it may not have been intended for us.
Logger.LogDebug(11, "message.State is null or empty."); 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. // 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) if (properties == null)
{ {
Logger.LogError(12, "Unable to unprotect the message.State."); 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 any of the error fields are set, throw error null
if (!string.IsNullOrEmpty(message.Error)) 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"); 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; string userstate = null;
@ -369,7 +369,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
if (!ValidateCorrelationId(properties)) if (!ValidateCorrelationId(properties))
{ {
return AuthenticateResult.Failed("Correlation failed."); return AuthenticateResult.Fail("Correlation failed.");
} }
if (_configuration == null && Options.ConfigurationManager != null) if (_configuration == null && Options.ConfigurationManager != null)
@ -393,7 +393,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
else if (authorizationResponseReceivedContext.Skipped) else if (authorizationResponseReceivedContext.Skipped)
{ {
Logger.LogDebug(17, "AuthorizationResponseReceived.Skipped"); Logger.LogDebug(17, "AuthorizationResponseReceived.Skipped");
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
message = authorizationResponseReceivedContext.ProtocolMessage; message = authorizationResponseReceivedContext.ProtocolMessage;
properties = authorizationResponseReceivedContext.Properties; properties = authorizationResponseReceivedContext.Properties;
@ -409,7 +409,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
else else
{ {
Logger.LogTrace(18, "Cannot process the message. Both id_token and code are missing."); 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) catch (Exception exception)
@ -433,7 +433,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (authenticationFailedContext.Skipped) else if (authenticationFailedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
throw; throw;
@ -459,7 +459,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (authorizationCodeReceivedContext.Skipped) else if (authorizationCodeReceivedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
message = authorizationCodeReceivedContext.ProtocolMessage; message = authorizationCodeReceivedContext.ProtocolMessage;
var code = authorizationCodeReceivedContext.Code; var code = authorizationCodeReceivedContext.Code;
@ -476,7 +476,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (authorizationCodeRedeemedContext.Skipped) else if (authorizationCodeRedeemedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
message = authorizationCodeRedeemedContext.ProtocolMessage; message = authorizationCodeRedeemedContext.ProtocolMessage;
@ -509,7 +509,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (authenticationValidatedContext.Skipped) else if (authenticationValidatedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
ticket = authenticationValidatedContext.AuthenticationTicket; ticket = authenticationValidatedContext.AuthenticationTicket;
@ -558,7 +558,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (authenticationValidatedContext.Skipped) else if (authenticationValidatedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
message = authenticationValidatedContext.ProtocolMessage; message = authenticationValidatedContext.ProtocolMessage;
ticket = authenticationValidatedContext.AuthenticationTicket; ticket = authenticationValidatedContext.AuthenticationTicket;
@ -573,7 +573,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (authorizationCodeReceivedContext.Skipped) else if (authorizationCodeReceivedContext.Skipped)
{ {
return AuthenticateResult.Success(ticket: null); return AuthenticateResult.Skip();
} }
message = authorizationCodeReceivedContext.ProtocolMessage; message = authorizationCodeReceivedContext.ProtocolMessage;
ticket = authorizationCodeReceivedContext.AuthenticationTicket; ticket = authorizationCodeReceivedContext.AuthenticationTicket;
@ -671,7 +671,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
} }
else if (userInformationReceivedContext.Skipped) else if (userInformationReceivedContext.Skipped)
{ {
return null; return ticket;
} }
ticket = userInformationReceivedContext.AuthenticationTicket; ticket = userInformationReceivedContext.AuthenticationTicket;
user = userInformationReceivedContext.User; user = userInformationReceivedContext.User;

View File

@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
if (requestToken == null) if (requestToken == null)
{ {
return AuthenticateResult.Failed("Invalid state cookie."); return AuthenticateResult.Fail("Invalid state cookie.");
} }
properties = requestToken.Properties; properties = requestToken.Properties;
@ -54,18 +54,18 @@ namespace Microsoft.AspNet.Authentication.Twitter
var returnedToken = query["oauth_token"]; var returnedToken = query["oauth_token"];
if (StringValues.IsNullOrEmpty(returnedToken)) if (StringValues.IsNullOrEmpty(returnedToken))
{ {
return AuthenticateResult.Failed("Missing oauth_token"); return AuthenticateResult.Fail("Missing oauth_token");
} }
if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal)) if (!string.Equals(returnedToken, requestToken.Token, StringComparison.Ordinal))
{ {
return AuthenticateResult.Failed("Unmatched token"); return AuthenticateResult.Fail("Unmatched token");
} }
var oauthVerifier = query["oauth_verifier"]; var oauthVerifier = query["oauth_verifier"];
if (StringValues.IsNullOrEmpty(oauthVerifier)) if (StringValues.IsNullOrEmpty(oauthVerifier))
{ {
return AuthenticateResult.Failed("Missing or blank oauth_verifier"); return AuthenticateResult.Fail("Missing or blank oauth_verifier");
} }
var cookieOptions = new CookieOptions var cookieOptions = new CookieOptions

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Security.Claims;
using Microsoft.AspNet.Http.Authentication;
namespace Microsoft.AspNet.Authentication namespace Microsoft.AspNet.Authentication
{ {
@ -31,9 +29,14 @@ namespace Microsoft.AspNet.Authentication
public AuthenticationTicket Ticket { get; private set; } public AuthenticationTicket Ticket { get; private set; }
/// <summary> /// <summary>
/// Holds error information caused by authentication. /// Holds failure information from the authentication.
/// </summary> /// </summary>
public Exception Error { get; private set; } public Exception Failure { get; private set; }
/// <summary>
/// Indicates that this stage of authentication was skipped by user intervention.
/// </summary>
public bool Skipped { get; private set; }
public static AuthenticateResult Success(AuthenticationTicket ticket) public static AuthenticateResult Success(AuthenticationTicket ticket)
{ {
@ -44,14 +47,19 @@ namespace Microsoft.AspNet.Authentication
return new AuthenticateResult() { Ticket = ticket }; 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) };
} }
} }

View File

@ -100,6 +100,10 @@ namespace Microsoft.AspNet.Authentication
if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate)) if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate))
{ {
var result = await HandleAuthenticateOnceAsync(); var result = await HandleAuthenticateOnceAsync();
if (result.Failure != null)
{
Logger.LogInformation(0, $"{Options.AuthenticationScheme} not authenticated: " + result.Failure.Message);
}
var ticket = result?.Ticket; var ticket = result?.Ticket;
if (ticket?.Principal != null) if (ticket?.Principal != null)
{ {
@ -200,9 +204,9 @@ namespace Microsoft.AspNet.Authentication
// Calling Authenticate more than once should always return the original value. // Calling Authenticate more than once should always return the original value.
var result = await HandleAuthenticateOnceAsync(); var result = await HandleAuthenticateOnceAsync();
if (result?.Error != null) if (result?.Failure != null)
{ {
context.Failed(result.Error); context.Failed(result.Failure);
} }
else else
{ {

View File

@ -2,25 +2,24 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Authentication namespace Microsoft.AspNet.Authentication
{ {
/// <summary> /// <summary>
/// Provides error context information to middleware providers. /// Provides failure context information to middleware providers.
/// </summary> /// </summary>
public class ErrorContext : BaseControlContext public class FailureContext : BaseControlContext
{ {
public ErrorContext(HttpContext context, Exception error) public FailureContext(HttpContext context, Exception failure)
: base(context) : base(context)
{ {
Error = error; Failure = failure;
} }
/// <summary> /// <summary>
/// User friendly error message for the error. /// User friendly error message for the error.
/// </summary> /// </summary>
public Exception Error { get; set; } public Exception Failure { get; set; }
} }
} }

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNet.Authentication
/// <summary> /// <summary>
/// Invoked when the remote authentication process has an error. /// Invoked when the remote authentication process has an error.
/// </summary> /// </summary>
Task RemoteError(ErrorContext context); Task RemoteFailure(FailureContext context);
/// <summary> /// <summary>
/// Invoked before sign in. /// Invoked before sign in.

View File

@ -8,17 +8,17 @@ namespace Microsoft.AspNet.Authentication
{ {
public class RemoteAuthenticationEvents : IRemoteAuthenticationEvents public class RemoteAuthenticationEvents : IRemoteAuthenticationEvents
{ {
public Func<ErrorContext, Task> OnRemoteError { get; set; } = context => Task.FromResult(0); public Func<FailureContext, Task> OnRemoteFailure { get; set; } = context => Task.FromResult(0);
public Func<TicketReceivedContext, Task> OnTicketReceived { get; set; } = context => Task.FromResult(0); public Func<TicketReceivedContext, Task> OnTicketReceived { get; set; } = context => Task.FromResult(0);
/// <summary> /// <summary>
/// Invoked when there is a remote error /// Invoked when there is a remote failure
/// </summary> /// </summary>
public virtual Task RemoteError(ErrorContext context) => OnRemoteError(context); public virtual Task RemoteFailure(FailureContext context) => OnRemoteFailure(context);
/// <summary> /// <summary>
/// Invoked after the remote ticket has been recieved. /// Invoked after the remote ticket has been received.
/// </summary> /// </summary>
public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context); public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context);
} }

View File

@ -22,11 +22,15 @@ namespace Microsoft.AspNet.Authentication
protected virtual async Task<bool> HandleRemoteCallbackAsync() protected virtual async Task<bool> HandleRemoteCallbackAsync()
{ {
var authResult = await HandleRemoteAuthenticateAsync(); var authResult = await HandleRemoteAuthenticateAsync();
if (authResult != null && authResult.Skipped)
{
return false;
}
if (authResult == null || !authResult.Succeeded) if (authResult == null || !authResult.Succeeded)
{ {
var errorContext = new ErrorContext(Context, authResult?.Error ?? new Exception("Invalid return state, unable to redirect.")); var errorContext = new FailureContext(Context, authResult?.Failure ?? new Exception("Invalid return state, unable to redirect."));
Logger.LogInformation("Error from RemoteAuthentication: " + errorContext.Error.Message); Logger.LogInformation("Error from RemoteAuthentication: " + errorContext.Failure.Message);
await Options.Events.RemoteError(errorContext); await Options.Events.RemoteFailure(errorContext);
if (errorContext.HandledResponse) if (errorContext.HandledResponse)
{ {
return true; return true;
@ -36,7 +40,7 @@ namespace Microsoft.AspNet.Authentication
return false; 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 // We have a ticket if we get here
@ -77,7 +81,7 @@ namespace Microsoft.AspNet.Authentication
protected override Task<AuthenticateResult> HandleAuthenticateAsync() protected override Task<AuthenticateResult> 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) protected override Task HandleSignOutAsync(SignOutContext context)

View File

@ -261,9 +261,9 @@ namespace Microsoft.AspNet.Authentication.Google
{ {
options.Events = new OAuthEvents() 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(); ctx.HandleResponse();
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -275,7 +275,7 @@ namespace Microsoft.AspNet.Authentication.Google
{ {
var transaction = await sendTask; var transaction = await sendTask;
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); 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 else
{ {
@ -389,9 +389,9 @@ namespace Microsoft.AspNet.Authentication.Google
{ {
options.Events = new OAuthEvents() 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(); ctx.HandleResponse();
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -412,7 +412,7 @@ namespace Microsoft.AspNet.Authentication.Google
{ {
var transaction = await sendTask; var transaction = await sendTask;
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); 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()); transaction.Response.Headers.GetValues("Location").First());
} }
else else
@ -444,9 +444,9 @@ namespace Microsoft.AspNet.Authentication.Google
{ {
options.Events = new OAuthEvents() 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(); ctx.HandleResponse();
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -466,7 +466,7 @@ namespace Microsoft.AspNet.Authentication.Google
{ {
var transaction = await sendTask; var transaction = await sendTask;
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); 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()); transaction.Response.Headers.GetValues("Location").First());
} }
else else
@ -737,9 +737,9 @@ namespace Microsoft.AspNet.Authentication.Google
options.ClientSecret = "Test Secret"; options.ClientSecret = "Test Secret";
options.Events = new OAuthEvents() 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(); ctx.HandleResponse();
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -751,7 +751,7 @@ namespace Microsoft.AspNet.Authentication.Google
"https://example.com/signin-google?code=TestCode"); "https://example.com/signin-google?code=TestCode");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); 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()); transaction.Response.Headers.GetValues("Location").First());
} }

View File

@ -11,6 +11,7 @@ using System.Xml.Linq;
using Microsoft.AspNet.Builder; using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Http.Features.Authentication;
using Microsoft.AspNet.TestHost; using Microsoft.AspNet.TestHost;
using Microsoft.AspNet.Testing.xunit; using Microsoft.AspNet.Testing.xunit;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -312,6 +313,163 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); 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 class InvalidTokenValidator : ISecurityTokenValidator
{ {
public InvalidTokenValidator() public InvalidTokenValidator()
@ -387,7 +545,17 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
app.Use(async (context, next) => 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 || if (context.User == null ||
context.User.Identity == null || context.User.Identity == null ||
@ -408,14 +576,12 @@ namespace Microsoft.AspNet.Authentication.JwtBearer
await context.Response.WriteAsync(identifier.Value); await context.Response.WriteAsync(identifier.Value);
} }
else if (context.Request.Path == new PathString("/unauthorized")) else if (context.Request.Path == new PathString("/unauthorized"))
{ {
// Simulate Authorization failure // Simulate Authorization failure
var result = await context.Authentication.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); var result = await context.Authentication.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
await context.Authentication.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme); await context.Authentication.ChallengeAsync(JwtBearerDefaults.AuthenticationScheme);
} }
else if (context.Request.Path == new PathString("/signIn")) else if (context.Request.Path == new PathString("/signIn"))
{ {
await Assert.ThrowsAsync<NotSupportedException>(() => context.Authentication.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal())); await Assert.ThrowsAsync<NotSupportedException>(() => context.Authentication.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal()));