#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.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());
}

View File

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

View File

@ -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("<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("</body></html>");
});

View File

@ -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<AuthenticationTicket> _cookieTicketTask;
private Task<AuthenticateResult> _readCookieTask;
private Task<AuthenticationTicket> EnsureCookieTicket()
private Task<AuthenticateResult> 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<AuthenticationTicket> ReadCookieTicket()
private async Task<AuthenticateResult> 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<AuthenticateResult> 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)

View File

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

View File

@ -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<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;
await Options.Events.Challenge(new JwtBearerChallengeContext(Context, Options));
Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
return false;
}

View File

@ -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);

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
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;

View File

@ -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

View File

@ -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; }
/// <summary>
/// Holds error information caused by authentication.
/// Holds failure information from the authentication.
/// </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)
{
@ -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) };
}
}

View File

@ -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
{

View File

@ -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
{
/// <summary>
/// Provides error context information to middleware providers.
/// Provides failure context information to middleware providers.
/// </summary>
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;
}
/// <summary>
/// User friendly error message for the error.
/// </summary>
public Exception Error { get; set; }
public Exception Failure { get; set; }
}
}

View File

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

View File

@ -8,17 +8,17 @@ namespace Microsoft.AspNet.Authentication
{
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);
/// <summary>
/// Invoked when there is a remote error
/// Invoked when there is a remote failure
/// </summary>
public virtual Task RemoteError(ErrorContext context) => OnRemoteError(context);
public virtual Task RemoteFailure(FailureContext context) => OnRemoteFailure(context);
/// <summary>
/// Invoked after the remote ticket has been recieved.
/// Invoked after the remote ticket has been received.
/// </summary>
public virtual Task TicketReceived(TicketReceivedContext context) => OnTicketReceived(context);
}

View File

@ -22,11 +22,15 @@ namespace Microsoft.AspNet.Authentication
protected virtual async Task<bool> 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<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)

View File

@ -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());
}

View File

@ -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<NotSupportedException>(() => context.Authentication.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal()));