#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.
// 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.Http;
using Microsoft.AspNetCore.Http.Authentication;
@ -16,5 +17,10 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
}
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.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
@ -103,7 +104,8 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
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.
if (Options.RefreshOnIssuerKeyNotFound && ex.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException)))
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
@ -183,7 +185,12 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
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);
if (eventContext.HandledResponse)
{
@ -195,11 +202,94 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
}
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;
}
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)
{
throw new NotSupportedException();

View File

@ -575,7 +575,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
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.
if (Options.RefreshOnIssuerKeyNotFound && exception.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException)))
if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException)
{
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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Xml.Linq;
@ -30,7 +31,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var options = new JwtBearerOptions
{
AutomaticAuthenticate = true,
Authority = "https://login.windows.net/tushartest.onmicrosoft.com",
Audience = "https://TusharTest.onmicrosoft.com/TodoListService-ManualJwt"
};
@ -45,10 +45,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact]
public async Task SignInThrows()
{
var server = CreateServer(new JwtBearerOptions
{
AutomaticAuthenticate = true
});
var server = CreateServer(new JwtBearerOptions());
var transaction = await server.SendAsync("https://example.com/signIn");
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
}
@ -56,10 +53,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact]
public async Task SignOutThrows()
{
var server = CreateServer(new JwtBearerOptions
{
AutomaticAuthenticate = true
});
var server = CreateServer(new JwtBearerOptions());
var transaction = await server.SendAsync("https://example.com/signOut");
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
}
@ -70,7 +64,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var server = CreateServer(new JwtBearerOptions
{
AutomaticAuthenticate = true,
Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
@ -117,10 +110,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact]
public async Task UnrecognizedTokenReceived()
{
var server = CreateServer(new JwtBearerOptions
{
AutomaticAuthenticate = true
});
var server = CreateServer(new JwtBearerOptions());
var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob");
Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode);
@ -130,16 +120,67 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
[Fact]
public async Task InvalidTokenReceived()
{
var options = new JwtBearerOptions
{
AutomaticAuthenticate = true
};
var options = new JwtBearerOptions();
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new InvalidTokenValidator());
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);
}
[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);
}
@ -148,7 +189,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var options = new JwtBearerOptions
{
AutomaticAuthenticate = true,
Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
@ -185,7 +225,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var options = new JwtBearerOptions()
{
AutomaticAuthenticate = true,
Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
@ -233,7 +272,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var server = CreateServer(new JwtBearerOptions
{
AutomaticAuthenticate = true,
Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
@ -266,7 +304,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var options = new JwtBearerOptions
{
AutomaticAuthenticate = true,
Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
@ -298,7 +335,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var options = new JwtBearerOptions
{
AutomaticAuthenticate = true,
Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
@ -330,8 +366,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
var server = CreateServer(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Events = new JwtBearerEvents()
{
OnChallenge = context =>
@ -352,8 +386,16 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
{
public InvalidTokenValidator()
{
ExceptionType = typeof(SecurityTokenException);
}
public InvalidTokenValidator(Type exceptionType)
{
ExceptionType = exceptionType;
}
public Type ExceptionType { get; set; }
public bool CanValidateToken => true;
public int MaximumTokenSizeInBytes
@ -366,7 +408,9 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer
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;
}
}