From 5170c31b2317332abcaecc406eb0f395ae65bf4b Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 7 Mar 2019 08:15:16 -0800 Subject: [PATCH] Add details to the JwtBearer error messages #4679 (#8259) --- .../JwtBearer/src/JwtBearerHandler.cs | 23 +++-- .../Authentication/test/JwtBearerTests.cs | 95 +++++++++++++++++-- 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs index 4f03ccd69a..72d3a3bd29 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Security.Claims; using System.Text; @@ -284,23 +285,25 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer // and we want to display the most specific message possible. switch (ex) { - case SecurityTokenInvalidAudienceException _: - messages.Add("The audience is invalid"); + case SecurityTokenInvalidAudienceException stia: + messages.Add($"The audience '{stia.InvalidAudience ?? "(null)"}' is invalid"); break; - case SecurityTokenInvalidIssuerException _: - messages.Add("The issuer is invalid"); + case SecurityTokenInvalidIssuerException stii: + messages.Add($"The issuer '{stii.InvalidIssuer ?? "(null)"}' is invalid"); break; case SecurityTokenNoExpirationException _: messages.Add("The token has no expiration"); break; - case SecurityTokenInvalidLifetimeException _: - messages.Add("The token lifetime is invalid"); + case SecurityTokenInvalidLifetimeException stil: + messages.Add("The token lifetime is invalid; NotBefore: " + + $"'{stil.NotBefore?.ToString(CultureInfo.InvariantCulture) ?? "(null)"}'" + + $", Expires: '{stil.Expires?.ToString(CultureInfo.InvariantCulture) ?? "(null)"}'"); break; - case SecurityTokenNotYetValidException _: - messages.Add("The token is not valid yet"); + case SecurityTokenNotYetValidException stnyv: + messages.Add($"The token is not valid before '{stnyv.NotBefore.ToString(CultureInfo.InvariantCulture)}'"); break; - case SecurityTokenExpiredException _: - messages.Add("The token is expired"); + case SecurityTokenExpiredException ste: + messages.Add($"The token expired at '{ste.Expires.ToString(CultureInfo.InvariantCulture)}'"); break; case SecurityTokenSignatureKeyNotFoundException _: messages.Add("The signature key was not found"); diff --git a/src/Security/Authentication/test/JwtBearerTests.cs b/src/Security/Authentication/test/JwtBearerTests.cs index ad77803cde..faa26d9f5d 100644 --- a/src/Security/Authentication/test/JwtBearerTests.cs +++ b/src/Security/Authentication/test/JwtBearerTests.cs @@ -239,12 +239,12 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer } [Theory] - [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience is invalid")] - [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer is invalid")] + [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience '(null)' is invalid")] + [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer '(null)' 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(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid; NotBefore: '(null)', Expires: '(null)'")] + [InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid before '01/01/0001 00:00:00'")] + [InlineData(typeof(SecurityTokenExpiredException), "The token expired at '01/01/0001 00:00:00'")] [InlineData(typeof(SecurityTokenInvalidSignatureException), "The signature is invalid")] [InlineData(typeof(SecurityTokenSignatureKeyNotFoundException), "The signature key was not found")] public async Task ExceptionReportedInHeaderForAuthenticationFailures(Type errorType, string message) @@ -261,6 +261,26 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer Assert.Equal("", response.ResponseText); } + [Theory] + [InlineData(typeof(SecurityTokenInvalidAudienceException), "The audience 'Bad Audience' is invalid")] + [InlineData(typeof(SecurityTokenInvalidIssuerException), "The issuer 'Bad Issuer' is invalid")] + [InlineData(typeof(SecurityTokenInvalidLifetimeException), "The token lifetime is invalid; NotBefore: '01/15/2001 00:00:00', Expires: '02/20/2000 00:00:00'")] + [InlineData(typeof(SecurityTokenNotYetValidException), "The token is not valid before '01/15/2045 00:00:00'")] + [InlineData(typeof(SecurityTokenExpiredException), "The token expired at '02/20/2000 00:00:00'")] + public async Task ExceptionReportedInHeaderWithDetailsForAuthenticationFailures(Type errorType, string message) + { + var server = CreateServer(options => + { + options.SecurityTokenValidators.Clear(); + options.SecurityTokenValidators.Add(new DetailedInvalidTokenValidator(errorType)); + }); + + 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) @@ -289,7 +309,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 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 '(null)' is invalid; The signature key was not found\"", response.Response.Headers.WwwAuthenticate.First().ToString()); Assert.Equal("", response.ResponseText); } @@ -691,6 +711,69 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer } } + class DetailedInvalidTokenValidator : ISecurityTokenValidator + { + public DetailedInvalidTokenValidator() + { + ExceptionType = typeof(SecurityTokenException); + } + + public DetailedInvalidTokenValidator(Type exceptionType) + { + ExceptionType = exceptionType; + } + + public Type ExceptionType { get; set; } + + public bool CanValidateToken => true; + + public int MaximumTokenSizeInBytes + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public bool CanReadToken(string securityToken) => true; + + public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken) + { + if (ExceptionType == typeof(SecurityTokenInvalidAudienceException)) + { + throw new SecurityTokenInvalidAudienceException("SecurityTokenInvalidAudienceException") { InvalidAudience = "Bad Audience" }; + } + if (ExceptionType == typeof(SecurityTokenInvalidIssuerException)) + { + throw new SecurityTokenInvalidIssuerException("SecurityTokenInvalidIssuerException") { InvalidIssuer = "Bad Issuer" }; + } + if (ExceptionType == typeof(SecurityTokenInvalidLifetimeException)) + { + throw new SecurityTokenInvalidLifetimeException("SecurityTokenInvalidLifetimeException") + { + NotBefore = new DateTime(2001, 1, 15), + Expires = new DateTime(2000, 2, 20), + }; + } + if (ExceptionType == typeof(SecurityTokenNotYetValidException)) + { + throw new SecurityTokenNotYetValidException("SecurityTokenNotYetValidException") + { + NotBefore = new DateTime(2045, 1, 15), + }; + } + if (ExceptionType == typeof(SecurityTokenExpiredException)) + { + throw new SecurityTokenExpiredException("SecurityTokenExpiredException") + { + Expires = new DateTime(2000, 2, 20), + }; + } + else + { + throw new NotImplementedException(ExceptionType.Name); + } + } + } + class BlobTokenValidator : ISecurityTokenValidator { private Action _tokenValidator;