From cfcd0b3dc7b715e921f28d463cf638a281676bf8 Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Fri, 8 Nov 2024 12:02:21 -0800 Subject: [PATCH] JsonWebTokenHandler IssuerExtensibility (#2987) * JsonWebTokenHandler IssuerExtensibility * Address PR Comments --------- Co-authored-by: id4s --- .../InternalAPI.Unshipped.txt | 3 +- ...nWebTokenHandler.ValidateToken.Internal.cs | 30 +- ...bTokenHandler.ValidateToken.StackFrames.cs | 1 - .../InternalAPI.Unshipped.txt | 5 + .../LogMessages.cs | 1 + .../Results/Details/IssuerValidationError.cs | 21 +- .../Validation/ValidationFailureType.cs | 6 + .../Validation/ValidationParameters.cs | 2 +- .../Validation/Validators.Issuer.cs | 8 +- ...sonWebTokenHandler.Issuer.Extensibility.cs | 290 ++++++++++++++++++ .../CustomExceptions.cs | 26 ++ .../CustomValidationDelegates.cs | 147 +++++++++ .../CustomValidationErrors.cs | 59 ++++ .../IdentityComparer.cs | 122 ++++++++ .../Validation/IssuerValidationResultTests.cs | 4 +- 15 files changed, 703 insertions(+), 22 deletions(-) create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.Issuer.Extensibility.cs create mode 100644 test/Microsoft.IdentityModel.TestUtils/CustomExceptions.cs create mode 100644 test/Microsoft.IdentityModel.TestUtils/CustomValidationDelegates.cs create mode 100644 test/Microsoft.IdentityModel.TestUtils/CustomValidationErrors.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt index 16af108561..81abc18294 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt @@ -1,3 +1,4 @@ static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.EncryptToken(byte[] innerTokenUtf8Bytes, Microsoft.IdentityModel.Tokens.EncryptingCredentials encryptingCredentials, string compressionAlgorithm, System.Collections.Generic.IDictionary additionalHeaderClaims, string tokenType, bool includeKeyIdInHeader) -> string static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.WriteJweHeader(Microsoft.IdentityModel.Tokens.EncryptingCredentials encryptingCredentials, string compressionAlgorithm, string tokenType, System.Collections.Generic.IDictionary jweHeaderClaims, bool includeKeyIdInHeader) -> byte[] -static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.WriteJwsHeader(ref System.Text.Json.Utf8JsonWriter writer, Microsoft.IdentityModel.Tokens.SigningCredentials signingCredentials, Microsoft.IdentityModel.Tokens.EncryptingCredentials encryptingCredentials, System.Collections.Generic.IDictionary jweHeaderClaims, System.Collections.Generic.IDictionary jwsHeaderClaims, string tokenType, bool includeKeyIdInHeader) -> void \ No newline at end of file +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.WriteJwsHeader(ref System.Text.Json.Utf8JsonWriter writer, Microsoft.IdentityModel.Tokens.SigningCredentials signingCredentials, Microsoft.IdentityModel.Tokens.EncryptingCredentials encryptingCredentials, System.Collections.Generic.IDictionary jweHeaderClaims, System.Collections.Generic.IDictionary jwsHeaderClaims, string tokenType, bool includeKeyIdInHeader) -> void +static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.StackFrames.IssuerValidatorThrew -> System.Diagnostics.StackFrame diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs index f99da5e6e2..18fa21fcf9 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs @@ -135,8 +135,7 @@ await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration, if (result.IsValid) return result; - StackFrame tokenValidationStackFrame = StackFrames.TokenValidationFailedNullConfigurationManager ??= new StackFrame(true); - return result.UnwrapError().AddStackFrame(tokenValidationStackFrame); + return result.UnwrapError().AddStackFrame(ValidationError.GetCurrentStackFrame()); } if (result.IsValid) @@ -277,14 +276,29 @@ private async ValueTask> ValidateJWSAsync( return audienceValidationResult.UnwrapError().AddStackFrame(audienceValidationFailureStackFrame); } - ValidationResult issuerValidationResult = await validationParameters.IssuerValidatorAsync( - jsonWebToken.Issuer, jsonWebToken, validationParameters, callContext, cancellationToken) - .ConfigureAwait(false); + ValidationResult issuerValidationResult; + try + { + issuerValidationResult = await validationParameters.IssuerValidatorAsync( + jsonWebToken.Issuer, jsonWebToken, validationParameters, callContext, cancellationToken) + .ConfigureAwait(false); - if (!issuerValidationResult.IsValid) + if (!issuerValidationResult.IsValid) + { + return issuerValidationResult.UnwrapError().AddCurrentStackFrame(); + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types { - StackFrame issuerValidationFailureStackFrame = StackFrames.IssuerValidationFailed ??= new StackFrame(true); - return issuerValidationResult.UnwrapError().AddStackFrame(issuerValidationFailureStackFrame); + return new IssuerValidationError( + new MessageDetail(TokenLogMessages.IDX10269), + ValidationFailureType.IssuerValidatorThrew, + typeof(SecurityTokenInvalidIssuerException), + ValidationError.GetCurrentStackFrame(), + jsonWebToken.Issuer, + ex); } ValidationResult replayValidationResult = validationParameters.TokenReplayValidator( diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.StackFrames.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.StackFrames.cs index f841d0b85c..69a02ccacb 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.StackFrames.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.StackFrames.cs @@ -22,7 +22,6 @@ internal static class StackFrames internal static StackFrame? TokenNull; internal static StackFrame? TokenValidationParametersNull; internal static StackFrame? TokenNotJWT; - internal static StackFrame? TokenValidationFailedNullConfigurationManager; internal static StackFrame? TokenValidationFailed; // ValidateJWEAsync internal static StackFrame? DecryptionFailed; diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index e7eaf3cd7f..fa2f518ab7 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -1,11 +1,15 @@ const Microsoft.IdentityModel.Tokens.LogMessages.IDX10002 = "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10268 = "IDX10268: Unable to validate audience, validationParameters.ValidAudiences.Count == 0." -> string +const Microsoft.IdentityModel.Tokens.LogMessages.IDX10269 = "IDX10269: IssuerValidationDelegate threw an exception, see inner exception." -> string Microsoft.IdentityModel.Tokens.AlgorithmValidationError Microsoft.IdentityModel.Tokens.AlgorithmValidationError.AlgorithmValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidAlgorithm) -> void Microsoft.IdentityModel.Tokens.AlgorithmValidationError.InvalidAlgorithm.get -> string Microsoft.IdentityModel.Tokens.AlgorithmValidationError._invalidAlgorithm -> string Microsoft.IdentityModel.Tokens.AudienceValidationError.AudienceValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType failureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, System.Collections.Generic.IList tokenAudiences, System.Collections.Generic.IList validAudiences) -> void Microsoft.IdentityModel.Tokens.AudienceValidationError.TokenAudiences.get -> System.Collections.Generic.IList +Microsoft.IdentityModel.Tokens.IssuerValidationError.InvalidIssuer.get -> string +Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer, System.Exception innerException) -> void +Microsoft.IdentityModel.Tokens.IssuerValidationError.IssuerValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidIssuer, System.Exception innerException) -> void Microsoft.IdentityModel.Tokens.IssuerValidationSource.IssuerMatchedConfiguration = 1 -> Microsoft.IdentityModel.Tokens.IssuerValidationSource Microsoft.IdentityModel.Tokens.IssuerValidationSource.IssuerMatchedValidationParameters = 2 -> Microsoft.IdentityModel.Tokens.IssuerValidationSource Microsoft.IdentityModel.Tokens.LifetimeValidationError._expires -> System.DateTime @@ -31,6 +35,7 @@ static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidationParamete static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidationParametersNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Utility.SerializeAsSingleCommaDelimitedString(System.Collections.Generic.IList strings) -> string static Microsoft.IdentityModel.Tokens.ValidationError.GetCurrentStackFrame(string filePath = "", int lineNumber = 0, int skipFrames = 1) -> System.Diagnostics.StackFrame +static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.IssuerValidatorThrew -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoTokenAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoValidationParameterAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureAlgorithmValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 5b4679d25a..59c48703f3 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -87,6 +87,7 @@ internal static class LogMessages //public const string IDX10266 = "IDX10266: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace, validationParameters.ValidIssuers is null or empty and ConfigurationManager is null."; public const string IDX10267 = "IDX10267: '{0}' has been called by a derived class '{1}' which has not implemented this method. For this call graph to succeed, '{1}' will need to implement '{0}'."; public const string IDX10268 = "IDX10268: Unable to validate audience, validationParameters.ValidAudiences.Count == 0."; + public const string IDX10269 = "IDX10269: IssuerValidationDelegate threw an exception, see inner exception."; // 10500 - SignatureValidation diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs index b707cbf459..cc26ebda9a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerValidationError.cs @@ -9,25 +9,36 @@ 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) + : this(messageDetail, ValidationFailureType.IssuerValidationFailed, exceptionType, stackFrame, invalidIssuer, null) { - _invalidIssuer = invalidIssuer; } + internal IssuerValidationError( + MessageDetail messageDetail, + ValidationFailureType validationFailureType, + Type exceptionType, + StackFrame stackFrame, + string? invalidIssuer, + Exception? innerException) + : base(messageDetail, validationFailureType, exceptionType, stackFrame, innerException) + { + InvalidIssuer = invalidIssuer; + } + + internal string? InvalidIssuer { get; } + internal override Exception GetException() { if (ExceptionType == typeof(SecurityTokenInvalidIssuerException)) { SecurityTokenInvalidIssuerException exception = new(MessageDetail.Message, InnerException) { - InvalidIssuer = _invalidIssuer + InvalidIssuer = InvalidIssuer }; return exception; diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index cd58d536c7..e512d2af5d 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -128,5 +128,11 @@ private class InvalidSecurityTokenFailure : ValidationFailureType { internal Inv /// public static readonly ValidationFailureType XmlValidationFailed = new XmlValidationFailure("XmlValidationFailed"); private class XmlValidationFailure : ValidationFailureType { internal XmlValidationFailure(string name) : base(name) { } } + + /// + /// Defines a type that represents that a token is invalid. + /// + public static readonly ValidationFailureType IssuerValidatorThrew = new IssuerValidatorFailure("IssuerValidatorThrew"); + private class IssuerValidatorFailure : ValidationFailureType { internal IssuerValidatorFailure(string name) : base(name) { } } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs index 100cddaad5..24fdaed4e5 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs @@ -311,7 +311,7 @@ public IssuerSigningKeyValidationDelegate IssuerSigningKeyValidator /// Allows overriding the delegate that will be used to validate the issuer of the token. /// /// Thrown when the value is set as null. - /// The used to validate the issuer of a token + /// The used to validate the issuer of a token public IssuerValidationDelegateAsync IssuerValidatorAsync { get { return _issuerValidatorAsync; } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs index 29abaa8125..8a2cd297b1 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -52,8 +52,8 @@ public static partial class Validators /// An that contains either the issuer that was validated or an error. /// An EXACT match is required. internal static async Task> ValidateIssuerAsync( - string issuer, - SecurityToken securityToken, + string? issuer, + SecurityToken? securityToken, ValidationParameters validationParameters, #pragma warning disable CA1801 // Review unused parameters CallContext? callContext, @@ -103,7 +103,7 @@ internal static async Task> ValidateIssuerAsyn // LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer), callContext); - return new ValidatedIssuer(issuer, IssuerValidationSource.IssuerMatchedConfiguration); + return new ValidatedIssuer(issuer!, IssuerValidationSource.IssuerMatchedConfiguration); } } @@ -126,7 +126,7 @@ internal static async Task> ValidateIssuerAsyn //if (LogHelper.IsEnabled(EventLogLevel.Informational)) // LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - return new ValidatedIssuer(issuer, IssuerValidationSource.IssuerMatchedValidationParameters); + return new ValidatedIssuer(issuer!, IssuerValidationSource.IssuerMatchedValidationParameters); } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.Issuer.Extensibility.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.Issuer.Extensibility.cs new file mode 100644 index 0000000000..b66d40b0f6 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.Issuer.Extensibility.cs @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens.Tests; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Json.Tests; +using Xunit; + +#nullable enable +namespace Microsoft.IdentityModel.JsonWebTokens.Extensibility.Tests +{ + public partial class JsonWebTokenHandlerValidateTokenAsyncTests + { + [Theory, MemberData(nameof(Issuer_ExtensibilityTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_IssuerValidator_Extensibility(IssuerExtensibilityTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.{nameof(ValidateTokenAsync_IssuerValidator_Extensibility)}", theoryData); + context.IgnoreType = false; + for (int i = 1; i < theoryData.StackFrames.Count; i++) + theoryData.IssuerValidationError!.AddStackFrame(theoryData.StackFrames[i]); + + try + { + ValidationResult validationResult = await theoryData.JsonWebTokenHandler.ValidateTokenAsync( + theoryData.JsonWebToken!, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + if (validationResult.IsValid) + { + ValidatedToken validatedToken = validationResult.UnwrapResult(); + if (validatedToken.ValidatedIssuer.HasValue) + IdentityComparer.AreValidatedIssuersEqual(validatedToken.ValidatedIssuer.Value, theoryData.ValidatedIssuer, context); + } + else + { + ValidationError validationError = validationResult.UnwrapError(); + IdentityComparer.AreValidationErrorsEqual(validationError, theoryData.IssuerValidationError, context); + theoryData.ExpectedException.ProcessException(validationError.GetException(), context); + } + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData Issuer_ExtensibilityTestCases + { + get + { + var theoryData = new TheoryData(); + CallContext callContext = new CallContext(); + string issuerGuid = Guid.NewGuid().ToString(); + + #region return CustomIssuerValidationError + // Test cases where delegate is overridden and return an CustomIssuerValidationError + // CustomIssuerValidationError : IssuerValidationError, ExceptionType: SecurityTokenInvalidIssuerException + theoryData.Add(new IssuerExtensibilityTheoryData( + "CustomIssuerValidatorDelegate", + issuerGuid, + CustomIssuerValidatorDelegates.CustomIssuerValidatorDelegateAsync, + [ + new StackFrame("CustomValidationDelegates.cs", 88), + new StackFrame(false), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenInvalidIssuerException), + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorDelegateAsync)), + IssuerValidationError = new CustomIssuerValidationError( + new MessageDetail( + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorDelegateAsync), null), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame("CustomValidationDelegates.cs", 88), + issuerGuid) + }); + + // CustomIssuerValidationError : IssuerValidationError, ExceptionType: CustomSecurityTokenInvalidIssuerException : SecurityTokenInvalidIssuerException + theoryData.Add(new IssuerExtensibilityTheoryData( + "CustomIssuerValidatorCustomExceptionDelegate", + issuerGuid, + CustomIssuerValidatorDelegates.CustomIssuerValidatorCustomExceptionDelegateAsync, + [ + new StackFrame("CustomValidationDelegates.cs", 107), + new StackFrame(false), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenInvalidIssuerException), + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorCustomExceptionDelegateAsync)), + IssuerValidationError = new CustomIssuerValidationError( + new MessageDetail( + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorCustomExceptionDelegateAsync), null), + typeof(CustomSecurityTokenInvalidIssuerException), + new StackFrame("CustomValidationDelegates.cs", 107), + issuerGuid), + }); + + // CustomIssuerValidationError : IssuerValidationError, ExceptionType: NotSupportedException : SystemException + theoryData.Add(new IssuerExtensibilityTheoryData( + "CustomIssuerValidatorUnknownExceptionDelegate", + issuerGuid, + CustomIssuerValidatorDelegates.CustomIssuerValidatorUnknownExceptionDelegateAsync, + [ + new StackFrame("CustomValidationDelegates.cs", 139), + new StackFrame(false), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenException), + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorUnknownExceptionDelegateAsync)), + IssuerValidationError = new CustomIssuerValidationError( + new MessageDetail( + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorUnknownExceptionDelegateAsync), null), + typeof(NotSupportedException), + new StackFrame("CustomValidationDelegates.cs", 139), + issuerGuid), + }); + + // CustomIssuerValidationError : IssuerValidationError, ExceptionType: NotSupportedException : SystemException, ValidationFailureType: CustomIssuerValidationFailureType + theoryData.Add(new IssuerExtensibilityTheoryData( + "CustomIssuerValidatorCustomExceptionCustomFailureTypeDelegate", + issuerGuid, + CustomIssuerValidatorDelegates.CustomIssuerValidatorCustomExceptionCustomFailureTypeDelegateAsync, + [ + new StackFrame("CustomValidationDelegates.cs", 123), + new StackFrame(false), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenInvalidIssuerException), + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorCustomExceptionCustomFailureTypeDelegateAsync)), + IssuerValidationError = new CustomIssuerValidationError( + new MessageDetail( + nameof(CustomIssuerValidatorDelegates.CustomIssuerValidatorCustomExceptionCustomFailureTypeDelegateAsync), null), + CustomIssuerValidationError.CustomIssuerValidationFailureType, + typeof(CustomSecurityTokenInvalidIssuerException), + new StackFrame("CustomValidationDelegates.cs", 123), + issuerGuid, + null), + }); + #endregion + + #region return IssuerValidationError + // Test cases where delegate is overridden and return an IssuerValidationError + // IssuerValidationError : ValidationError, ExceptionType: SecurityTokenInvalidIssuerException + theoryData.Add(new IssuerExtensibilityTheoryData( + "IssuerValidatorDelegate", + issuerGuid, + CustomIssuerValidatorDelegates.IssuerValidatorDelegateAsync, + [ + new StackFrame("CustomValidationDelegates.cs", 169), + new StackFrame(false), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenInvalidIssuerException), + nameof(CustomIssuerValidatorDelegates.IssuerValidatorDelegateAsync)), + IssuerValidationError = new IssuerValidationError( + new MessageDetail( + nameof(CustomIssuerValidatorDelegates.IssuerValidatorDelegateAsync), null), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame("CustomValidationDelegates.cs", 169), + issuerGuid) + }); + + // IssuerValidationError : ValidationError, ExceptionType: CustomSecurityTokenInvalidIssuerException : SecurityTokenInvalidIssuerException + theoryData.Add(new IssuerExtensibilityTheoryData( + "IssuerValidatorCustomIssuerExceptionTypeDelegate", + issuerGuid, + CustomIssuerValidatorDelegates.IssuerValidatorCustomIssuerExceptionTypeDelegateAsync, + [ + new StackFrame("CustomValidationDelegates.cs", 196), + new StackFrame(false), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenException), + nameof(CustomIssuerValidatorDelegates.IssuerValidatorCustomIssuerExceptionTypeDelegateAsync)), + IssuerValidationError = new IssuerValidationError( + new MessageDetail( + nameof(CustomIssuerValidatorDelegates.IssuerValidatorCustomIssuerExceptionTypeDelegateAsync), null), + typeof(CustomSecurityTokenInvalidIssuerException), + new StackFrame("CustomValidationDelegates.cs", 196), + issuerGuid) + }); + + // IssuerValidationError : ValidationError, ExceptionType: CustomSecurityTokenException : SystemException + theoryData.Add(new IssuerExtensibilityTheoryData( + "IssuerValidatorCustomExceptionTypeDelegate", + issuerGuid, + CustomIssuerValidatorDelegates.IssuerValidatorCustomExceptionTypeDelegateAsync, + [ + new StackFrame("CustomValidationDelegates.cs", 210), + new StackFrame(false), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenException), + nameof(CustomIssuerValidatorDelegates.IssuerValidatorCustomExceptionTypeDelegateAsync)), + IssuerValidationError = new IssuerValidationError( + new MessageDetail( + nameof(CustomIssuerValidatorDelegates.IssuerValidatorCustomExceptionTypeDelegateAsync), null), + typeof(CustomSecurityTokenException), + new StackFrame("CustomValidationDelegates.cs", 210), + issuerGuid) + }); + + // IssuerValidationError : ValidationError, ExceptionType: SecurityTokenInvalidIssuerException, inner: CustomSecurityTokenInvalidIssuerException + theoryData.Add(new IssuerExtensibilityTheoryData( + "IssuerValidatorThrows", + issuerGuid, + CustomIssuerValidatorDelegates.IssuerValidatorThrows, + [ + new StackFrame("JsonWebTokenHandler.ValidateToken.Internal.cs", 300), + new StackFrame(false) + ]) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenInvalidIssuerException), + string.Format(Tokens.LogMessages.IDX10269), + typeof(CustomSecurityTokenInvalidIssuerException)), + IssuerValidationError = new IssuerValidationError( + new MessageDetail( + string.Format(Tokens.LogMessages.IDX10269), null), + ValidationFailureType.IssuerValidatorThrew, + typeof(SecurityTokenInvalidIssuerException), + new StackFrame("JsonWebTokenHandler.ValidateToken.Internal.cs", 300), + issuerGuid, + new SecurityTokenInvalidIssuerException(nameof(CustomIssuerValidatorDelegates.IssuerValidatorThrows)) + ) + }); + #endregion + + return theoryData; + } + } + + public class IssuerExtensibilityTheoryData : ValidateTokenAsyncBaseTheoryData + { + internal IssuerExtensibilityTheoryData(string testId, string issuer, IssuerValidationDelegateAsync issuerValidator, IList stackFrames) : base(testId) + { + JsonWebToken = JsonUtilities.CreateUnsignedJsonWebToken("iss", issuer); + ValidationParameters = new ValidationParameters + { + AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation, + AudienceValidator = SkipValidationDelegates.SkipAudienceValidation, + IssuerValidatorAsync = issuerValidator, + IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation, + LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation, + SignatureValidator = SkipValidationDelegates.SkipSignatureValidation, + TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation, + TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation + }; + + StackFrames = stackFrames; + } + + public JsonWebToken JsonWebToken { get; } + + public JsonWebTokenHandler JsonWebTokenHandler { get; } = new JsonWebTokenHandler(); + + public bool IsValid { get; set; } + + internal ValidatedIssuer ValidatedIssuer { get; set; } + + internal IssuerValidationError? IssuerValidationError { get; set; } + + internal IList StackFrames { get; } + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/CustomExceptions.cs b/test/Microsoft.IdentityModel.TestUtils/CustomExceptions.cs new file mode 100644 index 0000000000..d1bfc42e68 --- /dev/null +++ b/test/Microsoft.IdentityModel.TestUtils/CustomExceptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.Tokens; + +#nullable enable +namespace Microsoft.IdentityModel.TestUtils +{ + internal class CustomSecurityTokenInvalidIssuerException : SecurityTokenInvalidIssuerException + { + public CustomSecurityTokenInvalidIssuerException(string message) + : base(message) + { + } + } + + internal class CustomSecurityTokenException : SystemException + { + public CustomSecurityTokenException(string message) + : base(message) + { + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/CustomValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/CustomValidationDelegates.cs new file mode 100644 index 0000000000..70f2082265 --- /dev/null +++ b/test/Microsoft.IdentityModel.TestUtils/CustomValidationDelegates.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +#nullable enable +namespace Microsoft.IdentityModel.TestUtils +{ + internal class CustomIssuerValidatorDelegates + { + internal async static Task> CustomIssuerValidatorDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + // Returns a CustomIssuerValidationError : IssuerValidationError + return await Task.FromResult(new ValidationResult( + new CustomIssuerValidationError( + new MessageDetail(nameof(CustomIssuerValidatorDelegateAsync), null), + typeof(SecurityTokenInvalidIssuerException), + ValidationError.GetCurrentStackFrame(), + issuer))); + } + + internal async static Task> CustomIssuerValidatorCustomExceptionDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + return await Task.FromResult(new ValidationResult( + new CustomIssuerValidationError( + new MessageDetail(nameof(CustomIssuerValidatorCustomExceptionDelegateAsync), null), + typeof(CustomSecurityTokenInvalidIssuerException), + ValidationError.GetCurrentStackFrame(), + issuer))); + } + + internal async static Task> CustomIssuerValidatorCustomExceptionCustomFailureTypeDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + return await Task.FromResult(new ValidationResult( + new CustomIssuerValidationError( + new MessageDetail(nameof(CustomIssuerValidatorCustomExceptionCustomFailureTypeDelegateAsync), null), + CustomIssuerValidationError.CustomIssuerValidationFailureType, + typeof(CustomSecurityTokenInvalidIssuerException), + ValidationError.GetCurrentStackFrame(), + issuer, + null))); + } + + internal async static Task> CustomIssuerValidatorUnknownExceptionDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + return await Task.FromResult(new ValidationResult( + new CustomIssuerValidationError( + new MessageDetail(nameof(CustomIssuerValidatorUnknownExceptionDelegateAsync), null), + typeof(NotSupportedException), + ValidationError.GetCurrentStackFrame(), + issuer))); + } + + internal async static Task> CustomIssuerValidatorWithoutGetExceptionOverrideDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + return await Task.FromResult(new ValidationResult( + new CustomIssuerWithoutGetExceptionValidationOverrideError( + new MessageDetail(nameof(CustomIssuerValidatorWithoutGetExceptionOverrideDelegateAsync), null), + typeof(CustomSecurityTokenInvalidIssuerException), + ValidationError.GetCurrentStackFrame(), + issuer))); + } + + internal async static Task> IssuerValidatorDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + return await Task.FromResult(new ValidationResult( + new IssuerValidationError( + new MessageDetail(nameof(IssuerValidatorDelegateAsync), null), + typeof(SecurityTokenInvalidIssuerException), + ValidationError.GetCurrentStackFrame(), + issuer))); + } + + internal static Task> IssuerValidatorThrows( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + throw new CustomSecurityTokenInvalidIssuerException(nameof(IssuerValidatorThrows)); + } + + internal async static Task> IssuerValidatorCustomIssuerExceptionTypeDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + return await Task.FromResult(new ValidationResult( + new IssuerValidationError( + new MessageDetail(nameof(IssuerValidatorCustomIssuerExceptionTypeDelegateAsync), null), + typeof(CustomSecurityTokenInvalidIssuerException), + ValidationError.GetCurrentStackFrame(), + issuer))); + } + internal async static Task> IssuerValidatorCustomExceptionTypeDelegateAsync( + string issuer, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + return await Task.FromResult(new ValidationResult( + new IssuerValidationError( + new MessageDetail(nameof(IssuerValidatorCustomExceptionTypeDelegateAsync), null), + typeof(CustomSecurityTokenException), + ValidationError.GetCurrentStackFrame(), + issuer))); + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/CustomValidationErrors.cs b/test/Microsoft.IdentityModel.TestUtils/CustomValidationErrors.cs new file mode 100644 index 0000000000..b8ecee5438 --- /dev/null +++ b/test/Microsoft.IdentityModel.TestUtils/CustomValidationErrors.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.IdentityModel.Tokens; + +#nullable enable +namespace Microsoft.IdentityModel.TestUtils +{ + internal class CustomIssuerValidationError : IssuerValidationError + { + /// + /// A custom validation failure type. + /// + public static readonly ValidationFailureType CustomIssuerValidationFailureType = new IssuerValidatorFailure("CustomIssuerValidationFailureType"); + private class IssuerValidatorFailure : ValidationFailureType { internal IssuerValidatorFailure(string name) : base(name) { } } + + public CustomIssuerValidationError( + MessageDetail messageDetail, + Type exceptionType, + StackFrame stackFrame, + string? invalidIssuer) + : base(messageDetail, exceptionType, stackFrame, invalidIssuer) + { + } + + public CustomIssuerValidationError( + MessageDetail messageDetail, + ValidationFailureType validationFailureType, + Type exceptionType, + StackFrame stackFrame, + string? invalidIssuer, + Exception? innerException) + : base(messageDetail, validationFailureType, exceptionType, stackFrame, invalidIssuer, innerException) + { + } + + internal override Exception GetException() + { + if (ExceptionType == typeof(CustomSecurityTokenInvalidIssuerException)) + return new CustomSecurityTokenInvalidIssuerException(MessageDetail.Message) { InvalidIssuer = InvalidIssuer }; + + return base.GetException(); + } + } + + internal class CustomIssuerWithoutGetExceptionValidationOverrideError : IssuerValidationError + { + public CustomIssuerWithoutGetExceptionValidationOverrideError(MessageDetail messageDetail, + Type exceptionType, + StackFrame stackFrame, + string? invalidIssuer) : + base(messageDetail, exceptionType, stackFrame, invalidIssuer) + { + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index ca2e264853..1ce815b2ea 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -1406,6 +1406,128 @@ internal static bool AreValidatedTokenTypesEqual(ValidatedTokenType validatedTok return context.Merge(localContext); } + internal static bool AreValidationErrorsEqual(ValidationError validationError1, ValidationError validationError2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(validationError1, validationError2, localContext)) + return context.Merge(localContext); + + if (validationError1.StackFrames[0] == null || validationError2.StackFrames[0] == null) + { + localContext.Diffs.Add($"(validationError1.StackFrames[0] is null || validationError2.StackFrames[0] is null."); + } + else + { + // It is assumed that validationError1 is the result from the validation call graph. + // validationError2 is set when building the test case. + // Check the number of frames and the first filename. + if (validationError1.StackFrames.Count != validationError2.StackFrames.Count) + localContext.Diffs.Add($"(validationError1.StackFrames.Count != validationError2.StackFrames.Count: {validationError1.StackFrames.Count}, {validationError2.StackFrames.Count})"); + + if (!validationError1.StackFrames[0].GetFileName().Contains(validationError2.StackFrames[0].GetFileName())) + { + localContext.Diffs.Add($"(validationError1.StackFrames[0].GetFileName(): " + + $"'{validationError1.StackFrames[0].GetFileName()}', " + + $"does not contain validationError2.StackFrames[0].GetFileName():" + + $"'{validationError1.StackFrames[0].GetFileName()}'."); + } + } + + AreStringsEqual( + validationError1.GetType().FullName, + validationError2.GetType().FullName, + "validationError1.GetType().FullName", + "validationError2.GetType().FullName", + localContext); + + AreStringsEqual( + validationError1.ExceptionType.ToString(), + validationError2.ExceptionType.ToString(), + "validationError1.ExceptionType", + "validationError2.ExceptionType", + localContext); + + AreStringsEqual( + validationError1.FailureType, + validationError2.FailureType, + "validationError1.FailureType", + "validationError2.FailureType", + localContext); + + // sometimes it is helpful to see the exception without stepping into the method, hence the locals. + Exception exception1 = validationError1.GetException(); + Exception exception2 = validationError2.GetException(); + + AreExceptionsEqual( + exception1, + exception2, + localContext); + + AreMessageDetailsEqual( + validationError1.MessageDetail, + validationError2.MessageDetail, + localContext); + + return context.Merge(localContext); + } + + internal static bool AreExceptionsEqual(Exception exception1, Exception exception2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(exception1, exception2, localContext)) + return context.Merge(localContext); + + AreStringsEqual( + exception1.GetType().ToString(), + exception2.GetType().ToString(), + "exception1.GetType().ToString()", + "exception2.GetType().ToString()", + localContext); + + AreStringsEqual( + exception1.Message, + exception2.Message, + "exception1.Message.ToString()", + "exception2.Message.ToString()", + localContext); + + if (exception1.GetType() == typeof(SecurityTokenInvalidIssuerException)) + { + AreStringsEqual( + ((SecurityTokenInvalidIssuerException)exception1).InvalidIssuer, + ((SecurityTokenInvalidIssuerException)exception2).InvalidIssuer, + "((SecurityTokenInvalidIssuerException)exception1).InvalidIssuer", + "((SecurityTokenInvalidIssuerException)exception2).InvalidIssuer", + localContext); + } + + return context.Merge(localContext); + } + + internal static bool AreMessageDetailsEqual(MessageDetail messageDetail1, MessageDetail messageDetail2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(messageDetail1, messageDetail2, localContext)) + return context.Merge(localContext); + + AreStringsEqual( + messageDetail1.GetType().ToString(), + messageDetail2.GetType().ToString(), + "messageDetail1.GetType().ToString()", + "messageDetail2.GetType().ToString()", + localContext); + + AreStringsEqual( + messageDetail1.Message, + messageDetail2.Message, + "messageDetail1.Message", + "messageDetail2.Message", + localContext); + + return context.Merge(localContext); + } + + private static bool AreValueCollectionsEqual(Object object1, Object object2, CompareContext context) { Dictionary.ValueCollection vc1 = (Dictionary.ValueCollection)object1; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs index abc1e39dc9..e16148631e 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -11,9 +11,9 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Xunit; -namespace Microsoft.IdentityModel.Tokens.Validation.Tests +namespace Microsoft.IdentityModel.Tokens.IssuerValidation.Tests { - public class IssuerValidationResultTests + public partial class IssuerValidationResultTests { [Theory, MemberData(nameof(IssuerValdationResultsTestCases), DisableDiscoveryEnumeration = true)] public async Task IssuerValidatorAsyncTests(IssuerValidationResultsTheoryData theoryData)