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; } }