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()
{