diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs index b4dcf1147b..5846812538 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerChallengeContext.cs @@ -22,5 +22,25 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer /// Any failures encountered during the authentication process. /// public Exception AuthenticateFailure { get; set; } + + /// + /// Gets or sets the "error" value returned to the caller as part + /// of the WWW-Authenticate header. This property may be null when + /// is set to false. + /// + public string Error { get; set; } + + /// + /// Gets or sets the "error_description" value returned to the caller as part + /// of the WWW-Authenticate header. This property may be null when + /// is set to false. + /// + public string ErrorDescription { get; set; } + + /// + /// Gets or sets the "error_uri" value returned to the caller as part of the + /// WWW-Authenticate header. This property is always null unless explicitly set. + /// + public string ErrorUri { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs index 077f1debdb..34b13562a5 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs @@ -191,6 +191,14 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer { AuthenticateFailure = authResult?.Failure, }; + + // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token). + if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null) + { + eventContext.Error = "invalid_token"; + eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure); + } + await Options.Events.Challenge(eventContext); if (eventContext.HandledResponse) { @@ -203,9 +211,9 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer Response.StatusCode = 401; - var errorDescription = CreateErrorDescription(eventContext.AuthenticateFailure); - - if (errorDescription.Length == 0) + if (string.IsNullOrEmpty(eventContext.Error) && + string.IsNullOrEmpty(eventContext.ErrorDescription) && + string.IsNullOrEmpty(eventContext.ErrorUri)) { Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge); } @@ -219,9 +227,35 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer // Only add a comma after the first param, if any builder.Append(','); } - builder.Append(" error=\"invalid_token\", error_description=\""); - builder.Append(errorDescription); - builder.Append('\"'); + if (!string.IsNullOrEmpty(eventContext.Error)) + { + builder.Append(" error=\""); + builder.Append(eventContext.Error); + builder.Append("\""); + } + if (!string.IsNullOrEmpty(eventContext.ErrorDescription)) + { + if (!string.IsNullOrEmpty(eventContext.Error)) + { + builder.Append(","); + } + + builder.Append(" error_description=\""); + builder.Append(eventContext.ErrorDescription); + builder.Append('\"'); + } + if (!string.IsNullOrEmpty(eventContext.ErrorUri)) + { + if (!string.IsNullOrEmpty(eventContext.Error) || + !string.IsNullOrEmpty(eventContext.ErrorDescription)) + { + builder.Append(","); + } + + builder.Append(" error_uri=\""); + builder.Append(eventContext.ErrorUri); + builder.Append('\"'); + } Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); } @@ -231,11 +265,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer private static string CreateErrorDescription(Exception authFailure) { - if (authFailure == null) - { - return string.Empty; - } - IEnumerable exceptions; if (authFailure is AggregateException) { diff --git a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs index 837928e777..1d73b843ee 100644 --- a/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerOptions.cs @@ -118,5 +118,12 @@ namespace Microsoft.AspNetCore.Builder /// after a successful authorization. /// public bool SaveToken { get; set; } = true; + + /// + /// Defines whether the token validation errors should be returned to the caller. + /// Enabled by default, this option can be disabled to prevent the JWT middleware + /// from returning an error and an error_description in the WWW-Authenticate header. + /// + public bool IncludeErrorDetails { get; set; } = true; } } diff --git a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs index 5a10ef616f..b1f0ce4fed 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/JwtBearer/JwtBearerMiddlewareTests.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Http; using System.Reflection; using System.Security.Claims; +using System.Text; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.AspNetCore.Builder; @@ -127,7 +128,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer 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("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); Assert.Equal("", response.ResponseText); } @@ -164,7 +165,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer 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("Bearer error=\"invalid_token\"", response.Response.Headers.WwwAuthenticate.First().ToString()); Assert.Equal("", response.ResponseText); } @@ -179,11 +180,100 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer 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\"", + 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); } + [Theory] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", null, null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, null, "custom_uri")] + public async Task ExceptionsReportedInHeaderExposesUserDefinedError(string error, string description, string uri) + { + var options = new JwtBearerOptions + { + Events = new JwtBearerEvents + { + OnChallenge = context => + { + context.Error = error; + context.ErrorDescription = description; + context.ErrorUri = uri; + + return Task.FromResult(0); + } + } + }; + var server = CreateServer(options); + + var response = await SendAsync(server, "http://example.com/oauth", "Bearer someblob"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("", response.ResponseText); + + var builder = new StringBuilder(options.Challenge); + + if (!string.IsNullOrEmpty(error)) + { + builder.Append(" error=\""); + builder.Append(error); + builder.Append("\""); + } + if (!string.IsNullOrEmpty(description)) + { + if (!string.IsNullOrEmpty(error)) + { + builder.Append(","); + } + + builder.Append(" error_description=\""); + builder.Append(description); + builder.Append('\"'); + } + if (!string.IsNullOrEmpty(uri)) + { + if (!string.IsNullOrEmpty(error) || + !string.IsNullOrEmpty(description)) + { + builder.Append(","); + } + + builder.Append(" error_uri=\""); + builder.Append(uri); + builder.Append('\"'); + } + + Assert.Equal(builder.ToString(), response.Response.Headers.WwwAuthenticate.First().ToString()); + } + + [Fact] + public async Task ExceptionNotReportedInHeaderWhenIncludeErrorDetailsIsFalse() + { + var server = CreateServer(new JwtBearerOptions + { + IncludeErrorDetails = false + }); + + 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 ExceptionNotReportedInHeaderWhenTokenWasMissing() + { + var server = CreateServer(new JwtBearerOptions()); + + var response = await SendAsync(server, "http://example.com/oauth"); + Assert.Equal(HttpStatusCode.Unauthorized, response.Response.StatusCode); + Assert.Equal("Bearer", response.Response.Headers.WwwAuthenticate.First().ToString()); + Assert.Equal("", response.ResponseText); + } + [Fact] public async Task CustomTokenValidated() {