#776 Show some JwtBearer errors in response headers
This commit is contained in:
parent
c9f8455dbc
commit
e299695974
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue