#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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -15,6 +16,8 @@ namespace JwtBearerSample
|
||||||
{
|
{
|
||||||
public Startup(IHostingEnvironment env)
|
public Startup(IHostingEnvironment env)
|
||||||
{
|
{
|
||||||
|
Environment = env;
|
||||||
|
|
||||||
var builder = new ConfigurationBuilder()
|
var builder = new ConfigurationBuilder()
|
||||||
.SetBasePath(env.ContentRootPath);
|
.SetBasePath(env.ContentRootPath);
|
||||||
|
|
||||||
|
|
@ -30,6 +33,8 @@ namespace JwtBearerSample
|
||||||
|
|
||||||
public IConfiguration Configuration { get; set; }
|
public IConfiguration Configuration { get; set; }
|
||||||
|
|
||||||
|
public IHostingEnvironment Environment { get; set; }
|
||||||
|
|
||||||
// Shared between users in memory
|
// Shared between users in memory
|
||||||
public IList<Todo> Todos { get; } = new List<Todo>();
|
public IList<Todo> Todos { get; } = new List<Todo>();
|
||||||
|
|
||||||
|
|
@ -68,7 +73,23 @@ namespace JwtBearerSample
|
||||||
{
|
{
|
||||||
// You also need to update /wwwroot/app/scripts/app.js
|
// You also need to update /wwwroot/app/scripts/app.js
|
||||||
Authority = Configuration["jwt:authority"],
|
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
|
// [Authorize] would usually handle this
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ namespace OpenIdConnectSample
|
||||||
{
|
{
|
||||||
public Startup(IHostingEnvironment env)
|
public Startup(IHostingEnvironment env)
|
||||||
{
|
{
|
||||||
|
Environment = env;
|
||||||
|
|
||||||
var builder = new ConfigurationBuilder()
|
var builder = new ConfigurationBuilder()
|
||||||
.SetBasePath(env.ContentRootPath);
|
.SetBasePath(env.ContentRootPath);
|
||||||
|
|
||||||
|
|
@ -35,6 +37,8 @@ namespace OpenIdConnectSample
|
||||||
|
|
||||||
public IConfiguration Configuration { get; set; }
|
public IConfiguration Configuration { get; set; }
|
||||||
|
|
||||||
|
public IHostingEnvironment Environment { get; set; }
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddAuthentication(sharedOptions =>
|
services.AddAuthentication(sharedOptions =>
|
||||||
|
|
@ -75,8 +79,24 @@ namespace OpenIdConnectSample
|
||||||
ClientId = Configuration["oidc:clientid"],
|
ClientId = Configuration["oidc:clientid"],
|
||||||
ClientSecret = Configuration["oidc:clientsecret"], // for code flow
|
ClientSecret = Configuration["oidc:clientsecret"], // for code flow
|
||||||
Authority = Configuration["oidc:authority"],
|
Authority = Configuration["oidc:authority"],
|
||||||
ResponseType = OpenIdConnectResponseType.Code,
|
ResponseType = OpenIdConnectResponseType.CodeIdToken,
|
||||||
GetClaimsFromUserInfoEndpoint = true
|
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 =>
|
app.Run(async context =>
|
||||||
|
|
|
||||||
|
|
@ -578,7 +578,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest);
|
tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, properties);
|
var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse, properties, ticket);
|
||||||
if (tokenResponseReceivedContext.CheckEventResult(out result))
|
if (tokenResponseReceivedContext.CheckEventResult(out result))
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -1038,13 +1038,15 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
private async Task<TokenResponseReceivedContext> RunTokenResponseReceivedEventAsync(
|
private async Task<TokenResponseReceivedContext> RunTokenResponseReceivedEventAsync(
|
||||||
OpenIdConnectMessage message,
|
OpenIdConnectMessage message,
|
||||||
OpenIdConnectMessage tokenEndpointResponse,
|
OpenIdConnectMessage tokenEndpointResponse,
|
||||||
AuthenticationProperties properties)
|
AuthenticationProperties properties,
|
||||||
|
AuthenticationTicket ticket)
|
||||||
{
|
{
|
||||||
Logger.TokenResponseReceived();
|
Logger.TokenResponseReceived();
|
||||||
var eventContext = new TokenResponseReceivedContext(Context, Options, properties)
|
var eventContext = new TokenResponseReceivedContext(Context, Options, properties)
|
||||||
{
|
{
|
||||||
ProtocolMessage = message,
|
ProtocolMessage = message,
|
||||||
TokenEndpointResponse = tokenEndpointResponse
|
TokenEndpointResponse = tokenEndpointResponse,
|
||||||
|
Ticket = ticket
|
||||||
};
|
};
|
||||||
|
|
||||||
await Options.Events.TokenResponseReceived(eventContext);
|
await Options.Events.TokenResponseReceived(eventContext);
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
public bool GetClaimsFromUserInfoEndpoint { get; set; }
|
public bool GetClaimsFromUserInfoEndpoint { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Exception Failure { get; private set; }
|
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>
|
/// <summary>
|
||||||
/// Indicates that this stage of authentication was skipped by user intervention.
|
/// Indicates that this stage of authentication was skipped by user intervention.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -47,6 +53,11 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
return new AuthenticateResult() { Ticket = ticket };
|
return new AuthenticateResult() { Ticket = ticket };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AuthenticateResult Handle()
|
||||||
|
{
|
||||||
|
return new AuthenticateResult() { Handled = true };
|
||||||
|
}
|
||||||
|
|
||||||
public static AuthenticateResult Skip()
|
public static AuthenticateResult Skip()
|
||||||
{
|
{
|
||||||
return new AuthenticateResult() { Skipped = true };
|
return new AuthenticateResult() { Skipped = true };
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
|
|
||||||
protected TOptions Options { get; private set; }
|
protected TOptions Options { get; private set; }
|
||||||
|
|
||||||
|
protected AuthenticateResult InitializeResult { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize is called once per request to contextualize this instance with appropriate state.
|
/// Initialize is called once per request to contextualize this instance with appropriate state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -101,12 +103,18 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
|
|
||||||
if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate))
|
if (ShouldHandleScheme(AuthenticationManager.AutomaticScheme, Options.AutomaticAuthenticate))
|
||||||
{
|
{
|
||||||
var result = await HandleAuthenticateOnceAsync();
|
InitializeResult = await HandleAuthenticateOnceAsync();
|
||||||
if (result?.Failure != null)
|
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)
|
if (ticket?.Principal != null)
|
||||||
{
|
{
|
||||||
Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal);
|
Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal);
|
||||||
|
|
@ -179,6 +187,10 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
/// pipeline.</returns>
|
/// pipeline.</returns>
|
||||||
public virtual Task<bool> HandleRequestAsync()
|
public virtual Task<bool> HandleRequestAsync()
|
||||||
{
|
{
|
||||||
|
if (InitializeResult?.Handled == true)
|
||||||
|
{
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,7 +262,7 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to ensure HandleAuthenticateAsync is only invoked once safely. The subsequent
|
/// Used to ensure HandleAuthenticateAsync is only invoked once safely. The subsequent
|
||||||
/// calls will return the same authentication result. Any exceptions will be converted
|
/// 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>
|
/// </summary>
|
||||||
protected async Task<AuthenticateResult> HandleAuthenticateOnceSafeAsync()
|
protected async Task<AuthenticateResult> HandleAuthenticateOnceSafeAsync()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,14 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
{
|
{
|
||||||
if (HandledResponse)
|
if (HandledResponse)
|
||||||
{
|
{
|
||||||
result = AuthenticateResult.Success(Ticket);
|
if (Ticket == null)
|
||||||
|
{
|
||||||
|
result = AuthenticateResult.Handle();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = AuthenticateResult.Success(Ticket);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (Skipped)
|
else if (Skipped)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
{
|
{
|
||||||
exception = new InvalidOperationException("Invalid return state, unable to redirect.");
|
exception = new InvalidOperationException("Invalid return state, unable to redirect.");
|
||||||
}
|
}
|
||||||
|
else if (authResult.Handled)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if (authResult.Skipped)
|
else if (authResult.Skipped)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,39 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
Assert.Equal(string.Empty, response.ResponseText);
|
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]
|
[Fact]
|
||||||
public async Task EventOnTokenValidatedSkipped_NoMoreEventsExecuted()
|
public async Task EventOnTokenValidatedSkipped_NoMoreEventsExecuted()
|
||||||
{
|
{
|
||||||
|
|
@ -460,6 +493,38 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
Assert.Equal(string.Empty, response.ResponseText);
|
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]
|
[Fact]
|
||||||
public async Task EventOnAuthenticationFailedSkipped_NoMoreEventsExecuted()
|
public async Task EventOnAuthenticationFailedSkipped_NoMoreEventsExecuted()
|
||||||
{
|
{
|
||||||
|
|
@ -491,6 +556,38 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
Assert.Equal(string.Empty, response.ResponseText);
|
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]
|
[Fact]
|
||||||
public async Task EventOnChallengeSkipped_ResponseNotModified()
|
public async Task EventOnChallengeSkipped_ResponseNotModified()
|
||||||
{
|
{
|
||||||
|
|
@ -512,6 +609,28 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
Assert.Equal(string.Empty, response.ResponseText);
|
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
|
class InvalidTokenValidator : ISecurityTokenValidator
|
||||||
{
|
{
|
||||||
public InvalidTokenValidator()
|
public InvalidTokenValidator()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue