From 876def8501e5c932d41568ff4d61e386126315e4 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Tue, 15 Oct 2024 16:15:45 +0100 Subject: [PATCH] Regression tests: Issuer (#2868) * Renamed CreateToken methods in audience and lifetime regression tests * Added custom ValidationError class for issuer errors. Updated IssuerValidationDelegateAsync to use it. * Added JWT issuer regression tests * Update test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Issuer.cs Co-authored-by: kellyyangsong <69649063+kellyyangsong@users.noreply.github.com> * Added IssuerValidationError to InternalAPI.Unshipped. Made constructor internal for the time being * Updated exception creation in IssuerValidationError * Adjusted unshipped API contents with the IDE suggestions --------- Co-authored-by: kellyyangsong <69649063+kellyyangsong@users.noreply.github.com> --- .../InternalAPI.Unshipped.txt | 3 +- .../net462/InternalAPI.Unshipped.txt | 4 +- .../net6.0/InternalAPI.Unshipped.txt | 4 +- .../net8.0/InternalAPI.Unshipped.txt | 4 +- .../net9.0/InternalAPI.Unshipped.txt | 4 +- .../netstandard2.0/InternalAPI.Unshipped.txt | 4 +- .../Results/Details/IssuerValidationError.cs | 40 +++++ .../Validation/Validators.Issuer.cs | 18 +-- ...andler.ValidateTokenAsyncTests.Audience.cs | 4 +- ...nHandler.ValidateTokenAsyncTests.Issuer.cs | 149 ++++++++++++++++++ ...andler.ValidateTokenAsyncTests.Lifetime.cs | 4 +- 11 files changed, 219 insertions(+), 19 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Issuer.cs diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index edb5c02a17..a9ee584268 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,4 +1,5 @@ -const Microsoft.IdentityModel.Tokens.LogMessages.IDX10001 = "IDX10001: Invalid argument '{0}'. Argument must be of type '{1}'." -> string +Microsoft.IdentityModel.Tokens.IssuerValidationError +const Microsoft.IdentityModel.Tokens.LogMessages.IDX10001 = "IDX10001: Invalid argument '{0}'. Argument must be of type '{1}'." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10502 = "IDX10502: Signature validation failed. The token's kid is: '{0}', but did not match any keys in ValidationParameters or Configuration and TryAllIssuerSigningKeys is false. Number of keys in ValidationParameters: '{1}'. \nNumber of keys in Configuration: '{2}'.\ntoken: '{3}'." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10518 = "IDX10518: Signature validation failed. Algorithm validation failed with error: '{0}'." -> string Microsoft.IdentityModel.Tokens.AlgorithmValidationDelegate diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/InternalAPI.Unshipped.txt index 596d061717..ceda8de7ed 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1,4 +1,6 @@ -Microsoft.IdentityModel.Tokens.AsymmetricAdapter.DecryptWithRsaCryptoServiceProviderProxy(byte[] bytes) -> byte[] +Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer) -> void +override Microsoft.IdentityModel.Tokens.IssuerValidationError.GetException() -> System.Exception +Microsoft.IdentityModel.Tokens.AsymmetricAdapter.DecryptWithRsaCryptoServiceProviderProxy(byte[] bytes) -> byte[] Microsoft.IdentityModel.Tokens.AsymmetricAdapter.EncryptWithRsaCryptoServiceProviderProxy(byte[] bytes) -> byte[] Microsoft.IdentityModel.Tokens.AsymmetricAdapter.SignWithRsaCryptoServiceProviderProxy(byte[] bytes) -> byte[] Microsoft.IdentityModel.Tokens.AsymmetricAdapter.SignWithRsaCryptoServiceProviderProxyUsingOffset(byte[] bytes, int offset, int length) -> byte[] diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/InternalAPI.Unshipped.txt index 34ab1b57d5..1182c74dc9 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/InternalAPI.Unshipped.txt @@ -1,3 +1,5 @@ -static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Generic.HashSet +Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer) -> void +override Microsoft.IdentityModel.Tokens.IssuerValidationError.GetException() -> System.Exception +static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Generic.HashSet Microsoft.IdentityModel.Tokens.EcdhKeyExchangeProvider.GetEncryptionAlgorithm() -> string Microsoft.IdentityModel.Tokens.SignUsingSpanDelegate diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 2a395cc172..0d34dedab5 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1,3 +1,5 @@ -static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Frozen.FrozenSet +Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer) -> void +override Microsoft.IdentityModel.Tokens.IssuerValidationError.GetException() -> System.Exception +static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Frozen.FrozenSet Microsoft.IdentityModel.Tokens.EcdhKeyExchangeProvider.GetEncryptionAlgorithm() -> string Microsoft.IdentityModel.Tokens.SignUsingSpanDelegate diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 2a395cc172..0d34dedab5 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1,3 +1,5 @@ -static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Frozen.FrozenSet +Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer) -> void +override Microsoft.IdentityModel.Tokens.IssuerValidationError.GetException() -> System.Exception +static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Frozen.FrozenSet Microsoft.IdentityModel.Tokens.EcdhKeyExchangeProvider.GetEncryptionAlgorithm() -> string Microsoft.IdentityModel.Tokens.SignUsingSpanDelegate diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index cdc130fb32..805ac7633a 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ -static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Generic.HashSet +Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer) -> void +override Microsoft.IdentityModel.Tokens.IssuerValidationError.GetException() -> System.Exception +static readonly Microsoft.IdentityModel.Tokens.Json.JsonWebKeySerializer.JsonWebKeyParameterNamesUpperCase -> System.Collections.Generic.HashSet diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs new file mode 100644 index 0000000000..8b4ff174b9 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens +{ + internal class IssuerValidationError : ValidationError + { + private string? _invalidIssuer; + + internal IssuerValidationError( + MessageDetail messageDetail, + Type exceptionType, + StackFrame stackFrame, + string? invalidIssuer) + : base(messageDetail, ValidationFailureType.IssuerValidationFailed, exceptionType, stackFrame) + { + _invalidIssuer = invalidIssuer; + } + + public override Exception GetException() + { + if (ExceptionType == typeof(SecurityTokenInvalidIssuerException)) + { + SecurityTokenInvalidIssuerException exception = new(MessageDetail.Message, InnerException) + { + InvalidIssuer = _invalidIssuer + }; + + return exception; + } + + return base.GetException(); + } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs index 8ea1aa26b1..c7782d5e2a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -61,11 +61,11 @@ internal static async Task> ValidateIssuerAsyn { if (string.IsNullOrWhiteSpace(issuer)) { - return new ValidationError( + return new IssuerValidationError( new MessageDetail(LogMessages.IDX10211), - ValidationFailureType.IssuerValidationFailed, typeof(SecurityTokenInvalidIssuerException), - new StackFrame(true)); + new StackFrame(true), + issuer); } if (validationParameters == null) @@ -84,11 +84,11 @@ internal static async Task> ValidateIssuerAsyn // Return failed IssuerValidationResult if all possible places to validate against are null or empty. if (validationParameters.ValidIssuers.Count == 0 && string.IsNullOrWhiteSpace(configuration?.Issuer)) - return new ValidationError( + return new IssuerValidationError( new MessageDetail(LogMessages.IDX10211), - ValidationFailureType.IssuerValidationFailed, typeof(SecurityTokenInvalidIssuerException), - new StackFrame(true)); + new StackFrame(true), + issuer); if (configuration != null) { @@ -130,15 +130,15 @@ internal static async Task> ValidateIssuerAsyn } } - return new ValidationError( + return new IssuerValidationError( new MessageDetail( LogMessages.IDX10212, LogHelper.MarkAsNonPII(issuer), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), LogHelper.MarkAsNonPII(configuration?.Issuer)), - ValidationFailureType.IssuerValidationFailed, typeof(SecurityTokenInvalidIssuerException), - new StackFrame(true)); + new StackFrame(true), + issuer); } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Audience.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Audience.cs index ca309dbd0c..be14c04424 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Audience.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Audience.cs @@ -17,7 +17,7 @@ public async Task ValidateTokenAsync_Audience(ValidateTokenAsyncAudienceTheoryDa { var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_Audience", theoryData); - string jwtString = CreateToken(theoryData.Audience); + string jwtString = CreateTokenWithAudience(theoryData.Audience); await ValidateAndCompareResults(jwtString, theoryData, context); @@ -155,7 +155,7 @@ public ValidateTokenAsyncAudienceTheoryData(string testId) : base(testId) { } public string? Audience { get; internal set; } = Default.Audience; } - private static string CreateToken(string? audience) + private static string CreateTokenWithAudience(string? audience) { JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Issuer.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Issuer.cs new file mode 100644 index 0000000000..a7fccd28dc --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Issuer.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + public partial class JsonWebTokenHandlerValidateTokenAsyncTests + { + [Theory, MemberData(nameof(ValidateTokenAsync_IssuerTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_Issuer(ValidateTokenAsyncIssuerTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_Issuer", theoryData); + + string jwtString = CreateTokenWithIssuer(theoryData.TokenIssuer); + + await ValidateAndCompareResults(jwtString, theoryData, context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateTokenAsync_IssuerTestCases + { + get + { + return new TheoryData + { + new ValidateTokenAsyncIssuerTheoryData("Valid_IssuerIsValidIssuer") + { + TokenIssuer = Default.Issuer, + TokenValidationParameters = CreateTokenValidationParameters(validIssuer: Default.Issuer), + ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer), + }, + new ValidateTokenAsyncIssuerTheoryData("Valid_IssuerIsConfigurationIssuer") + { + TokenIssuer = Default.Issuer, + TokenValidationParameters = CreateTokenValidationParameters(configurationIssuer: Default.Issuer), + ValidationParameters = CreateValidationParameters(configurationIssuer: Default.Issuer), + }, + new ValidateTokenAsyncIssuerTheoryData("Invalid_IssuerIsNotValid") + { + TokenIssuer = "InvalidIssuer", + TokenValidationParameters = CreateTokenValidationParameters(validIssuer: Default.Issuer), + ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer), + ExpectedIsValid = false, + ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10205:"), + ExpectedExceptionValidationParameters = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10212:"), + }, + new ValidateTokenAsyncIssuerTheoryData("Invalid_IssuerIsNull") + { + TokenIssuer = null, + TokenValidationParameters = CreateTokenValidationParameters(validIssuer: Default.Issuer), + ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer), + ExpectedIsValid = false, + ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10211:"), + }, + new ValidateTokenAsyncIssuerTheoryData("Invalid_IssuerIsEmpty") + { + TokenIssuer = string.Empty, + TokenValidationParameters = CreateTokenValidationParameters(validIssuer: Default.Issuer), + ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer), + ExpectedIsValid = false, + ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10211:"), + }, + new ValidateTokenAsyncIssuerTheoryData("Invalid_NoValidIssuersProvided") + { + TokenIssuer = Default.Issuer, + TokenValidationParameters = CreateTokenValidationParameters(), + ValidationParameters = CreateValidationParameters(), + ExpectedIsValid = false, + ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10204:"), + ExpectedExceptionValidationParameters = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10211:"), + }, + }; + + static TokenValidationParameters CreateTokenValidationParameters( + string? validIssuer = null, string? configurationIssuer = null) + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = true, + ValidateIssuer = true, + ValidateLifetime = true, + ValidateTokenReplay = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidAudiences = [Default.Audience], + ValidIssuer = validIssuer + }; + + if (configurationIssuer is not null) + { + var validConfig = new OpenIdConnectConfiguration() { Issuer = configurationIssuer }; + tokenValidationParameters.ConfigurationManager = new MockConfigurationManager(validConfig); + } + + return tokenValidationParameters; + } + + static ValidationParameters CreateValidationParameters( + string? validIssuer = null, string? configurationIssuer = null) + { + ValidationParameters validationParameters = new ValidationParameters(); + validationParameters.ValidAudiences.Add(Default.Audience); + validationParameters.IssuerSigningKeys.Add(Default.AsymmetricSigningKey); + + if (configurationIssuer is not null) + { + var validConfig = new OpenIdConnectConfiguration() { Issuer = configurationIssuer }; + validationParameters.ConfigurationManager = new MockConfigurationManager(validConfig); + } + + if (validIssuer is not null) + validationParameters.ValidIssuers.Add(validIssuer); + + return validationParameters; + } + } + } + + public class ValidateTokenAsyncIssuerTheoryData : ValidateTokenAsyncBaseTheoryData + { + public ValidateTokenAsyncIssuerTheoryData(string testId) : base(testId) { } + + public string? TokenIssuer { get; set; } + } + + private static string CreateTokenWithIssuer(string? issuer) + { + JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + Subject = Default.ClaimsIdentity, + SigningCredentials = Default.AsymmetricSigningCredentials, + Audience = Default.Audience, + Issuer = issuer, + }; + + return jsonWebTokenHandler.CreateToken(securityTokenDescriptor); + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.cs index acd5104844..89268e8b71 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Lifetime.cs @@ -17,7 +17,7 @@ public async Task ValidateTokenAsync_Lifetime(ValidateTokenAsyncLifetimeTheoryDa { var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_Lifetime", theoryData); - string jwtString = CreateToken(theoryData.IssuedAt, theoryData.NotBefore, theoryData.Expires); + string jwtString = CreateTokenWithLifetime(theoryData.IssuedAt, theoryData.NotBefore, theoryData.Expires); await ValidateAndCompareResults(jwtString, theoryData, context); @@ -155,7 +155,7 @@ public ValidateTokenAsyncLifetimeTheoryData(string testId) : base(testId) { } public DateTime? Expires { get; set; } } - private static string CreateToken(DateTime? issuedAt, DateTime? notBefore, DateTime? expires) + private static string CreateTokenWithLifetime(DateTime? issuedAt, DateTime? notBefore, DateTime? expires) { JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); jsonWebTokenHandler.SetDefaultTimesOnTokenCreation = false; // Allow for null values to be passed in to validate.