#776 Show some JwtBearer errors in response headers

This commit is contained in:
Chris R 2016-06-01 07:10:16 -07:00
parent c9f8455dbc
commit e299695974
4 changed files with 171 additions and 31 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Http.Authentication;
@ -16,5 +17,10 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
} }
public AuthenticationProperties Properties { get; } public AuthenticationProperties Properties { get; }
/// <summary>
/// Any failures encountered during the authentication process.
/// </summary>
public Exception AuthenticateFailure { get; set; }
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -103,7 +104,8 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
Logger.TokenValidationFailed(token, ex); Logger.TokenValidationFailed(token, ex);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
if (Options.RefreshOnIssuerKeyNotFound && ex.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{ {
Options.ConfigurationManager.RequestRefresh(); Options.ConfigurationManager.RequestRefresh();
} }
@ -183,7 +185,12 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
protected override async Task<bool> HandleUnauthorizedAsync(ChallengeContext context) protected override async Task<bool> HandleUnauthorizedAsync(ChallengeContext context)
{ {
var eventContext = new JwtBearerChallengeContext(Context, Options, new AuthenticationProperties(context.Properties)); var authResult = await HandleAuthenticateOnceAsync();
var eventContext = new JwtBearerChallengeContext(Context, Options, new AuthenticationProperties(context.Properties))
{
AuthenticateFailure = authResult?.Failure,
};
await Options.Events.Challenge(eventContext); await Options.Events.Challenge(eventContext);
if (eventContext.HandledResponse) if (eventContext.HandledResponse)
{ {
@ -195,11 +202,94 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
} }
Response.StatusCode = 401; Response.StatusCode = 401;
Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
var errorDescription = CreateErrorDescription(eventContext.AuthenticateFailure);
if (errorDescription.Length == 0)
{
Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
}
else
{
// https://tools.ietf.org/html/rfc6750#section-3.1
// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
var builder = new StringBuilder(Options.Challenge);
if (Options.Challenge.IndexOf(" ", StringComparison.Ordinal) > 0)
{
// Only add a comma after the first param, if any
builder.Append(',');
}
builder.Append(" error=\"invalid_token\", error_description=\"");
builder.Append(errorDescription);
builder.Append('\"');
Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
}
return false; return false;
} }
private static string CreateErrorDescription(Exception authFailure)
{
if (authFailure == null)
{
return string.Empty;
}
IEnumerable<Exception> exceptions;
if (authFailure is AggregateException)
{
var agEx = authFailure as AggregateException;
exceptions = agEx.InnerExceptions;
}
else
{
exceptions = new[] { authFailure };
}
var messages = new List<string>();
foreach (var ex in exceptions)
{
// Order sensitive, some of these exceptions derive from others
// and we want to display the most specific message possible.
if (ex is SecurityTokenInvalidAudienceException)
{
messages.Add("The audience is invalid");
}
else if (ex is SecurityTokenInvalidIssuerException)
{
messages.Add("The issuer is invalid");
}
else if (ex is SecurityTokenNoExpirationException)
{
messages.Add("The token has no expiration");
}
else if (ex is SecurityTokenInvalidLifetimeException)
{
messages.Add("The token lifetime is invalid");
}
else if (ex is SecurityTokenNotYetValidException)
{
messages.Add("The token is not valid yet");
}
else if (ex is SecurityTokenExpiredException)
{
messages.Add("The token is expired");
}
else if (ex is SecurityTokenSignatureKeyNotFoundException)
{
messages.Add("The signature key was not found");
}
else if (ex is SecurityTokenInvalidSignatureException)
{
messages.Add("The signature is invalid");
}
}
return string.Join("; ", messages);
}
protected override Task HandleSignOutAsync(SignOutContext context) protected override Task HandleSignOutAsync(SignOutContext context)
{ {
throw new NotSupportedException(); throw new NotSupportedException();

View File

@ -575,7 +575,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
Logger.ExceptionProcessingMessage(exception); Logger.ExceptionProcessingMessage(exception);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event. // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException)
{ {
if (Options.ConfigurationManager != null) if (Options.ConfigurationManager != null)
{ {

View File

@ -2,9 +2,10 @@
// 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.Collections.Generic; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Reflection;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
@ -30,7 +31,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var options = new JwtBearerOptions var options = new JwtBearerOptions
{ {
AutomaticAuthenticate = true,
Authority = "https://login.windows.net/tushartest.onmicrosoft.com", Authority = "https://login.windows.net/tushartest.onmicrosoft.com",
Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt" Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"
}; };
@ -45,10 +45,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact] [Fact]
public async Task SignInThrows() public async Task SignInThrows()
{ {
var server = CreateServer(new JwtBearerOptions var server = CreateServer(new JwtBearerOptions());
{
AutomaticAuthenticate = true
});
var transaction = await server.SendAsync("https://example.com/signIn"); var transaction = await server.SendAsync("https://example.com/signIn");
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
} }
@ -56,10 +53,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact] [Fact]
public async Task SignOutThrows() public async Task SignOutThrows()
{ {
var server = CreateServer(new JwtBearerOptions var server = CreateServer(new JwtBearerOptions());
{
AutomaticAuthenticate = true
});
var transaction = await server.SendAsync("https://example.com/signOut"); var transaction = await server.SendAsync("https://example.com/signOut");
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
} }
@ -70,7 +64,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var server = CreateServer(new JwtBearerOptions var server = CreateServer(new JwtBearerOptions
{ {
AutomaticAuthenticate = true,
Events = new JwtBearerEvents() Events = new JwtBearerEvents()
{ {
OnMessageReceived = context => OnMessageReceived = context =>
@ -117,10 +110,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact] [Fact]
public async Task UnrecognizedTokenReceived() public async Task UnrecognizedTokenReceived()
{ {
var server = CreateServer(new JwtBearerOptions var server = CreateServer(new JwtBearerOptions());
{
AutomaticAuthenticate = true
});
var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
@ -130,16 +120,67 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact] [Fact]
public async Task InvalidTokenReceived() public async Task InvalidTokenReceived()
{ {
var options = new JwtBearerOptions var options = new JwtBearerOptions();
{
AutomaticAuthenticate = true
};
options.SecurityTokenValidators.Clear(); options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new InvalidTokenValidator()); options.SecurityTokenValidators.Add(new InvalidTokenValidator());
var server = CreateServer(options); var server = CreateServer(options);
var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString());
Assert.Equal("", response.ResponseText);
}
[Theory]
[InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience is invalid")]
[InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer is invalid")]
[InlineData(typeof(SecurityTokenNoExpirationException), "The token has no expiration")]
[InlineData(typeof(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid")]
[InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid yet")]
[InlineData(typeof(SecurityTokenExpiredException), "The token is expired")]
[InlineData(typeof(SecurityTokenInvalidSignatureException), "The signature is invalid")]
[InlineData(typeof(SecurityTokenSignatureKeyNotFoundException), "The signature key was not found")]
public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorType, string message)
{
var options = new JwtBearerOptions();
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType));
var server = CreateServer(options);
var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
Assert.Equal($"Bearer error=\"invalid_token\", error_description=\"{message}\"", response.Response.Headers.WwwAuthenticate.First().ToString());
Assert.Equal("", response.ResponseText);
}
[Theory]
[InlineData(typeof(ArgumentException))]
public async Task ExceptionNotReportedInHeaderForOtherFailures(Type errorType)
{
var options = new JwtBearerOptions();
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new InvalidTokenValidator(errorType));
var server = CreateServer(options);
var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString());
Assert.Equal("", response.ResponseText);
}
[Fact]
public async Task ExceptionsReportedInHeaderForMultipleAuthenticationFailures()
{
var options = new JwtBearerOptions();
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenInvalidAudienceException)));
options.SecurityTokenValidators.Add(new InvalidTokenValidator(typeof(SecurityTokenSignatureKeyNotFoundException)));
var server = CreateServer(options);
var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
Assert.Equal($"Bearer error=\"invalid_token\", error_description=\"The audience is invalid; The signature key was not found\"",
response.Response.Headers.WwwAuthenticate.First().ToString());
Assert.Equal("", response.ResponseText); Assert.Equal("", response.ResponseText);
} }
@ -148,7 +189,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var options = new JwtBearerOptions var options = new JwtBearerOptions
{ {
AutomaticAuthenticate = true,
Events = new JwtBearerEvents() Events = new JwtBearerEvents()
{ {
OnTokenValidated = context => OnTokenValidated = context =>
@ -185,7 +225,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var options = new JwtBearerOptions() var options = new JwtBearerOptions()
{ {
AutomaticAuthenticate = true,
Events = new JwtBearerEvents() Events = new JwtBearerEvents()
{ {
OnMessageReceived = context => OnMessageReceived = context =>
@ -233,7 +272,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var server = CreateServer(new JwtBearerOptions var server = CreateServer(new JwtBearerOptions
{ {
AutomaticAuthenticate = true,
Events = new JwtBearerEvents() Events = new JwtBearerEvents()
{ {
OnMessageReceived = context => OnMessageReceived = context =>
@ -266,7 +304,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var options = new JwtBearerOptions var options = new JwtBearerOptions
{ {
AutomaticAuthenticate = true,
Events = new JwtBearerEvents() Events = new JwtBearerEvents()
{ {
OnTokenValidated = context => OnTokenValidated = context =>
@ -298,7 +335,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var options = new JwtBearerOptions var options = new JwtBearerOptions
{ {
AutomaticAuthenticate = true,
Events = new JwtBearerEvents() Events = new JwtBearerEvents()
{ {
OnTokenValidated = context => OnTokenValidated = context =>
@ -330,8 +366,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
var server = CreateServer(new JwtBearerOptions var server = CreateServer(new JwtBearerOptions
{ {
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Events = new JwtBearerEvents() Events = new JwtBearerEvents()
{ {
OnChallenge = context => OnChallenge = context =>
@ -352,8 +386,16 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{ {
public InvalidTokenValidator() public InvalidTokenValidator()
{ {
ExceptionType = typeof(SecurityTokenException);
} }
public InvalidTokenValidator(Type exceptionType)
{
ExceptionType = exceptionType;
}
public Type ExceptionType { get; set; }
public bool CanValidateToken => true; public bool CanValidateToken => true;
public int MaximumTokenSizeInBytes public int MaximumTokenSizeInBytes
@ -366,7 +408,9 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{ {
throw new SecurityTokenException("InvalidToken"); var constructor = ExceptionType.GetTypeInfo().GetConstructor(new[] { typeof(string) });
var exception = (Exception)constructor.Invoke(new[] { ExceptionType.Name });
throw exception;
} }
} }