#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.
|
// 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue