#884 Honor OIDC's and Jwt's OnAuthenticationFailed HandleResponse()

This commit is contained in:
Chris R 2016-10-17 14:58:08 -07:00
parent 8fcbddc23b
commit 2d1c56ce5c
10 changed files with 1743 additions and 12 deletions

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
@ -15,6 +16,8 @@ namespace JwtBearerSample
{
public Startup(IHostingEnvironment env)
{
Environment = env;
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath);
@ -30,6 +33,8 @@ namespace JwtBearerSample
public IConfiguration Configuration { get; set; }
public IHostingEnvironment Environment { get; set; }
// Shared between users in memory
public IList<Todo> Todos { get; } = new List<Todo>();
@ -68,7 +73,23 @@ namespace JwtBearerSample
{
// You also need to update /wwwroot/app/scripts/app.js
Authority = Configuration["jwt:authority"],
Audience = Configuration["jwt:audience"]
Audience = Configuration["jwt:audience"],
Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.HandleResponse();
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
if (Environment.IsDevelopment())
{
// Debug only, in production do not share exceptions with the remote host.
return c.Response.WriteAsync(c.Exception.ToString());
}
return c.Response.WriteAsync("An error occurred processing your authentication.");
}
}
});
// [Authorize] would usually handle this

View File

@ -20,6 +20,8 @@ namespace OpenIdConnectSample
{
public Startup(IHostingEnvironment env)
{
Environment = env;
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath);
@ -35,6 +37,8 @@ namespace OpenIdConnectSample
public IConfiguration Configuration { get; set; }
public IHostingEnvironment Environment { get; set; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
@ -75,8 +79,24 @@ namespace OpenIdConnectSample
ClientId = Configuration["oidc:clientid"],
ClientSecret = Configuration["oidc:clientsecret"], // for code flow
Authority = Configuration["oidc:authority"],
ResponseType = OpenIdConnectResponseType.Code,
GetClaimsFromUserInfoEndpoint = true
ResponseType = OpenIdConnectResponseType.CodeIdToken,
GetClaimsFromUserInfoEndpoint = true,
Events = new OpenIdConnectEvents()
{
OnAuthenticationFailed = c =>
{
c.HandleResponse();
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
if (Environment.IsDevelopment())
{
// Debug only, in production do not share exceptions with the remote host.
return c.Response.WriteAsync(c.Exception.ToString());
}
return c.Response.WriteAsync("An error occurred processing your authentication.");
}
}
});
app.Run(async context =>

View File

@ -578,7 +578,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest);
}
var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, properties);
var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, properties, ticket);
if (tokenResponseReceivedContext.CheckEventResult(out result))
{
return result;
@ -1038,13 +1038,15 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
private async Task<TokenResponseReceivedContext> RunTokenResponseReceivedEventAsync(
OpenIdConnectMessage message,
OpenIdConnectMessage tokenEndpointResponse,
AuthenticationProperties properties)
AuthenticationProperties properties,
AuthenticationTicket ticket)
{
Logger.TokenResponseReceived();
var eventContext = new TokenResponseReceivedContext(Context, Options, properties)
{
ProtocolMessage = message,
TokenEndpointResponse = tokenEndpointResponse
TokenEndpointResponse = tokenEndpointResponse,
Ticket = ticket
};
await Options.Events.TokenResponseReceived(eventContext);

View File

@ -86,6 +86,7 @@ namespace Microsoft.AspNetCore.Builder
/// <summary>
/// Boolean to set whether the middleware should go to user info endpoint to retrieve additional claims or not after creating an identity from id_token received from token endpoint.
/// The default is 'false'.
/// </summary>
public bool GetClaimsFromUserInfoEndpoint { get; set; }

View File

@ -33,6 +33,12 @@ namespace Microsoft.AspNetCore.Authentication
/// </summary>
public Exception Failure { get; private set; }
/// <summary>
/// Indicates that stage of authentication was directly handled by user intervention and no
/// further processing should be attempted.
/// </summary>
public bool Handled { get; private set; }
/// <summary>
/// Indicates that this stage of authentication was skipped by user intervention.
/// </summary>
@ -47,6 +53,11 @@ namespace Microsoft.AspNetCore.Authentication
return new AuthenticateResult() { Ticket = ticket };
}
public static AuthenticateResult Handle()
{
return new AuthenticateResult() { Handled = true };
}
public static AuthenticateResult Skip()
{
return new AuthenticateResult() { Skipped = true };

View File

@ -58,6 +58,8 @@ namespace Microsoft.AspNetCore.Authentication
protected TOptions Options { get; private set; }
protected AuthenticateResult InitializeResult { get; private set; }
/// <summary>
/// Initialize is called once per request to contextualize this instance with appropriate state.
/// </summary>
@ -101,12 +103,18 @@ namespace Microsoft.AspNetCore.Authentication
if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate))
{
var result = await HandleAuthenticateOnceAsync();
if (result?.Failure != null)
InitializeResult = await HandleAuthenticateOnceAsync();
if (InitializeResult?.Skipped == true || InitializeResult?.Handled == true)
{
Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Options.AuthenticationScheme, result.Failure.Message);
return;
}
var ticket = result?.Ticket;
if (InitializeResult?.Failure != null)
{
Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Options.AuthenticationScheme, InitializeResult.Failure.Message);
}
var ticket = InitializeResult?.Ticket;
if (ticket?.Principal != null)
{
Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal);
@ -179,6 +187,10 @@ namespace Microsoft.AspNetCore.Authentication
/// pipeline.</returns>
public virtual Task<bool> HandleRequestAsync()
{
if (InitializeResult?.Handled == true)
{
return Task.FromResult(true);
}
return Task.FromResult(false);
}
@ -250,7 +262,7 @@ namespace Microsoft.AspNetCore.Authentication
/// <summary>
/// Used to ensure HandleAuthenticateAsync is only invoked once safely. The subsequent
/// calls will return the same authentication result. Any exceptions will be converted
/// into a failed authenticatoin result containing the exception.
/// into a failed authentication result containing the exception.
/// </summary>
protected async Task<AuthenticateResult> HandleAuthenticateOnceSafeAsync()
{

View File

@ -51,7 +51,14 @@ namespace Microsoft.AspNetCore.Authentication
{
if (HandledResponse)
{
result = AuthenticateResult.Success(Ticket);
if (Ticket == null)
{
result = AuthenticateResult.Handle();
}
else
{
result = AuthenticateResult.Success(Ticket);
}
return true;
}
else if (Skipped)

View File

@ -43,6 +43,10 @@ namespace Microsoft.AspNetCore.Authentication
{
exception = new InvalidOperationException("Invalid return state, unable to redirect.");
}
else if (authResult.Handled)
{
return true;
}
else if (authResult.Skipped)
{
return false;

View File

@ -429,6 +429,39 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
Assert.Equal(string.Empty, response.ResponseText);
}
[Fact]
public async Task EventOnMessageReceivedHandled_NoMoreEventsExecuted()
{
var server = CreateServer(new JwtBearerOptions
{
Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status202Accepted;
return Task.FromResult(0);
},
OnTokenValidated = 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.Accepted, response.Response.StatusCode);
Assert.Equal(string.Empty, response.ResponseText);
}
[Fact]
public async Task EventOnTokenValidatedSkipped_NoMoreEventsExecuted()
{
@ -460,6 +493,38 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
Assert.Equal(string.Empty, response.ResponseText);
}
[Fact]
public async Task EventOnTokenValidatedHandled_NoMoreEventsExecuted()
{
var options = new JwtBearerOptions
{
Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status202Accepted;
return Task.FromResult(0);
},
OnAuthenticationFailed = context =>
{
throw new NotImplementedException(context.Exception.ToString());
},
OnChallenge = context =>
{
throw new NotImplementedException();
},
}
};
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT"));
var server = CreateServer(options);
var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
Assert.Equal(HttpStatusCode.Accepted, response.Response.StatusCode);
Assert.Equal(string.Empty, response.ResponseText);
}
[Fact]
public async Task EventOnAuthenticationFailedSkipped_NoMoreEventsExecuted()
{
@ -491,6 +556,38 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
Assert.Equal(string.Empty, response.ResponseText);
}
[Fact]
public async Task EventOnAuthenticationFailedHandled_NoMoreEventsExecuted()
{
var options = new JwtBearerOptions
{
Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
throw new Exception("Test Exception");
},
OnAuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status202Accepted;
return Task.FromResult(0);
},
OnChallenge = context =>
{
throw new NotImplementedException();
},
}
};
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new BlobTokenValidator("JWT"));
var server = CreateServer(options);
var response = await SendAsync(server, "http://example.com/checkforerrors", "Bearer Token");
Assert.Equal(HttpStatusCode.Accepted, response.Response.StatusCode);
Assert.Equal(string.Empty, response.ResponseText);
}
[Fact]
public async Task EventOnChallengeSkipped_ResponseNotModified()
{
@ -512,6 +609,28 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
Assert.Equal(string.Empty, response.ResponseText);
}
[Fact]
public async Task EventOnChallengeHandled_ResponseNotModified()
{
var server = CreateServer(new JwtBearerOptions
{
Events = new JwtBearerEvents()
{
OnChallenge = context =>
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status202Accepted;
return Task.FromResult(0);
},
}
});
var response = await SendAsync(server, "http://example.com/unauthorized", "Bearer Token");
Assert.Equal(HttpStatusCode.Accepted, response.Response.StatusCode);
Assert.Empty(response.Response.Headers.WwwAuthenticate);
Assert.Equal(string.Empty, response.ResponseText);
}
class InvalidTokenValidator : ISecurityTokenValidator
{
public InvalidTokenValidator()