diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs
index b3a4d21ba6..b4dcf1147b 100644
--- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs
+++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs
@@ -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; }
+
+ ///
+ /// Any failures encountered during the authentication process.
+ ///
+ public Exception AuthenticateFailure { get; set; }
}
}
diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs
index 40a0b2efd7..077f1debdb 100644
--- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs
+++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs
@@ -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 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 exceptions;
+ if (authFailure is AggregateException)
+ {
+ var agEx = authFailure as AggregateException;
+ exceptions = agEx.InnerExceptions;
+ }
+ else
+ {
+ exceptions = new[] { authFailure };
+ }
+
+ var messages = new List();
+
+ 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();
diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs
index e23173ed18..c674e21077 100644
--- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs
+++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs
@@ -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)
{
diff --git a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs
index 81c03dc83a..5a10ef616f 100644
--- a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs
+++ b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs
@@ -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;
}
}