#884 Honor OIDC's and Jwt's OnAuthenticationFailed HandleResponse()
This commit is contained in:
parent
8fcbddc23b
commit
2d1c56ce5c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue