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.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index 748018b3ab..1fc631663c 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -21,6 +21,7 @@ Microsoft.IdentityModel.Tokens.Saml2.Saml2ValidationError.Saml2ValidationError(M Microsoft.IdentityModel.Tokens.Saml2.Saml2ValidationError.Saml2ValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType failureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, System.Exception innerException) -> void override Microsoft.IdentityModel.Tokens.Saml.SamlValidationError.GetException() -> System.Exception override Microsoft.IdentityModel.Tokens.Saml2.Saml2ValidationError.GetException() -> System.Exception +static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.IssuerSigningKeyValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.IssuerValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.SignatureValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateSignature(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult @@ -38,6 +39,7 @@ static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AudienceValidationFailed -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.IssuerSigningKeyValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.IssuerValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.LifetimeValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.OneTimeUseValidationFailed -> System.Diagnostics.StackFrame diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs index b5115646f3..7d45db493a 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs @@ -57,7 +57,7 @@ internal async Task> ValidateTokenAsync( StackFrames.TokenValidationParametersNull); } - var conditionsResult = ValidateConditions(samlToken, validationParameters, callContext); + ValidationResult conditionsResult = ValidateConditions(samlToken, validationParameters, callContext); if (!conditionsResult.IsValid) { @@ -65,7 +65,7 @@ internal async Task> ValidateTokenAsync( return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed); } - var issuerValidationResult = await validationParameters.IssuerValidatorAsync( + ValidationResult issuerValidationResult = await validationParameters.IssuerValidatorAsync( samlToken.Issuer, samlToken, validationParameters, @@ -78,13 +78,27 @@ internal async Task> ValidateTokenAsync( return issuerValidationResult.UnwrapError().AddStackFrame(StackFrames.IssuerValidationFailed); } - var signatureValidationResult = ValidateSignature(samlToken, validationParameters, callContext); + ValidationResult signatureValidationResult = ValidateSignature(samlToken, validationParameters, callContext); + if (!signatureValidationResult.IsValid) { StackFrames.SignatureValidationFailed ??= new StackFrame(true); return signatureValidationResult.UnwrapError().AddStackFrame(StackFrames.SignatureValidationFailed); } + ValidationResult issuerSigningKeyValidationResult = validationParameters.IssuerSigningKeyValidator( + samlToken.SigningKey, + samlToken, + validationParameters, + null, + callContext); + + if (!issuerSigningKeyValidationResult.IsValid) + { + StackFrames.IssuerSigningKeyValidationFailed ??= new StackFrame(true); + return issuerSigningKeyValidationResult.UnwrapError().AddStackFrame(StackFrames.IssuerSigningKeyValidationFailed); + } + return new ValidatedToken(samlToken, this, validationParameters); } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs index faa6aad0a6..2108a5c662 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs @@ -14,6 +14,9 @@ internal static class StackFrames // Stack frames from ValidateTokenAsync using SecurityToken internal static StackFrame? TokenNull; internal static StackFrame? TokenValidationParametersNull; + internal static StackFrame? IssuerValidationFailed; + internal static StackFrame? IssuerSigningKeyValidationFailed; + internal static StackFrame? SignatureValidationFailed; // Stack frames from ValidateConditions internal static StackFrame? AudienceValidationFailed; @@ -22,9 +25,6 @@ internal static class StackFrames internal static StackFrame? AssertionConditionsValidationFailed; internal static StackFrame? LifetimeValidationFailed; internal static StackFrame? OneTimeUseValidationFailed; - - internal static StackFrame? IssuerValidationFailed; - internal static StackFrame? SignatureValidationFailed; } } } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs index d806f6a453..2ff1a01def 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -89,6 +89,19 @@ internal async Task> ValidateTokenAsync( return signatureValidationResult.UnwrapError().AddStackFrame(StackFrames.SignatureValidationFailed); } + var issuerSigningKeyValidationResult = validationParameters.IssuerSigningKeyValidator( + samlToken.SigningKey, + samlToken, + validationParameters, + null, + callContext); + + if (!issuerSigningKeyValidationResult.IsValid) + { + StackFrames.IssuerSigningKeyValidationFailed ??= new StackFrame(true); + return issuerSigningKeyValidationResult.UnwrapError().AddStackFrame(StackFrames.IssuerSigningKeyValidationFailed); + } + return new ValidatedToken(samlToken, this, validationParameters); } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs index 5638bf2f11..42c3ee2832 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs @@ -14,6 +14,7 @@ internal static class StackFrames // Stack frames from ValidateTokenAsync using SecurityToken internal static StackFrame? TokenNull; internal static StackFrame? TokenValidationParametersNull; + internal static StackFrame? IssuerSigningKeyValidationFailed; internal static StackFrame? IssuerValidationFailed; internal static StackFrame? SignatureValidationFailed; 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.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.IssuerSigningKey.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.IssuerSigningKey.cs new file mode 100644 index 0000000000..94d38d6151 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.IssuerSigningKey.cs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Saml2; +using Xunit; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml.Tests +{ + public partial class Saml2SecurityTokenHandlerTests + { + [Theory, MemberData(nameof(ValidateTokenAsync_IssuerSigningKey_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_IssuerSigningKeyComparison(ValidateTokenAsyncIssuerSigningKeyTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_IssuerSigningKeyComparison", theoryData); + + Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); + + var saml2Token = CreateTokenForSignatureValidation(theoryData.SigningCredentials); + + // Validate the token using TokenValidationParameters + TokenValidationResult tokenValidationResult = + await saml2TokenHandler.ValidateTokenAsync(saml2Token.Assertion.CanonicalString, theoryData.TokenValidationParameters!); + + // Validate the token using ValidationParameters. + ValidationResult validationResult = + await saml2TokenHandler.ValidateTokenAsync( + saml2Token, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + // Ensure the validity of the results match the expected result. + if (tokenValidationResult.IsValid != validationResult.IsValid) + { + context.AddDiff($"tokenValidationResult.IsValid != validationResult.IsSuccess"); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context); + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + } + else + { + if (tokenValidationResult.IsValid) + { + // Verify that the validated tokens from both paths match. + ValidatedToken validatedToken = validationResult.UnwrapResult(); + IdentityComparer.AreEqual(validatedToken.SecurityToken, tokenValidationResult.SecurityToken, context); + } + else + { + // Verify the exception provided by both paths match. + var tokenValidationResultException = tokenValidationResult.Exception; + theoryData.ExpectedException.ProcessException(tokenValidationResultException, context); + var validationResultException = validationResult.UnwrapError().GetException(); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResultException, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + } + + public static TheoryData ValidateTokenAsync_IssuerSigningKey_TestCases + { + get + { + int currentYear = DateTime.UtcNow.Year; + // Mock time provider, 100 years in the future + TimeProvider futureTimeProvider = new MockTimeProvider(new DateTimeOffset(currentYear + 100, 1, 1, 0, 0, 0, new(0))); + // Mock time provider, 100 years in the past + TimeProvider pastTimeProvider = new MockTimeProvider(new DateTimeOffset(currentYear - 100, 9, 16, 0, 0, 0, new(0))); + + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Valid_IssuerSigningKeyIsValid") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters(KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key), + ValidationParameters = CreateValidationParameters(KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_IssuerSigningKeyIsExpired") + { + // Signing key is valid between September 2011 and December 2039 + // Mock time provider is set to 100 years in the future, after the key expired + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, futureTimeProvider), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, futureTimeProvider), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10249:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10249:"), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_IssuerSigningKeyNotYetValid") + { + // Signing key is valid between September 2011 and December 2039 + // Mock time provider is set to 100 years in the past, before the key was valid. + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, pastTimeProvider), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, pastTimeProvider), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10248:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10248:"), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_TokenValidationParametersAndValidationParametersAreNull") + { + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + ExpectedIsValid = false, + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters( + SecurityKey issuerSigingKey, TimeProvider? timeProvider = null) + { + ValidationParameters validationParameters = new ValidationParameters(); + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + validationParameters.TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + validationParameters.SignatureValidator = ( + SecurityToken token, + ValidationParameters validationParameters, + BaseConfiguration? configuration, + CallContext? callContext) => + { + // Set the signing key for validation + token.SigningKey = issuerSigingKey; + return issuerSigingKey; + }; + + if (issuerSigingKey is not null) + validationParameters.IssuerSigningKeys.Add(issuerSigingKey); + + if (timeProvider is not null) + validationParameters.TimeProvider = timeProvider; + + return validationParameters; + } + + static TokenValidationParameters CreateTokenValidationParameters( + SecurityKey? issuerSigningKey = null, TimeProvider? timeProvider = null) + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + RequireAudience = false, + IssuerSigningKey = issuerSigningKey, + }; + + tokenValidationParameters.SignatureValidator = (token, tokenValidationParameters) => + { + // Set the signing key for validation + Saml2SecurityTokenHandler saml2SecurityTokenHandler = new Saml2SecurityTokenHandler(); + Saml2SecurityToken saml2SecurityToken = saml2SecurityTokenHandler.ReadSaml2Token(token); + saml2SecurityToken.SigningKey = issuerSigningKey; + + return saml2SecurityToken; + }; + + if (timeProvider is not null) + tokenValidationParameters.TimeProvider = timeProvider; + + return tokenValidationParameters; + } + } + } + + public class ValidateTokenAsyncIssuerSigningKeyTheoryData : TheoryDataBase + { + public ValidateTokenAsyncIssuerSigningKeyTheoryData(string testId) : base(testId) { } + + internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = ExpectedException.NoExceptionExpected; + + internal bool ExpectedIsValid { get; set; } = true; + + internal ValidationParameters? ValidationParameters { get; set; } + + internal TokenValidationParameters? TokenValidationParameters { get; set; } + + internal SigningCredentials? SigningCredentials { get; set; } + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.IssuerSigningKey.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.IssuerSigningKey.cs new file mode 100644 index 0000000000..f34c777f4f --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.IssuerSigningKey.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Xunit; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml.Tests +{ + public partial class SamlSecurityTokenHandlerTests + { + [Theory, MemberData(nameof(ValidateTokenAsync_IssuerSigningKey_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_IssuerSigningKeyComparison(ValidateTokenAsyncIssuerSigningKeyTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_IssuerSigningKeyComparison", theoryData); + + SamlSecurityTokenHandler samlTokenHandler = new SamlSecurityTokenHandler(); + + var samlToken = CreateTokenForSignatureValidation(theoryData.SigningCredentials); + + // Validate the token using TokenValidationParameters + TokenValidationResult tokenValidationResult = + await samlTokenHandler.ValidateTokenAsync(samlToken.Assertion.CanonicalString, theoryData.TokenValidationParameters!); + + // Validate the token using ValidationParameters. + ValidationResult validationResult = + await samlTokenHandler.ValidateTokenAsync( + samlToken, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + // Ensure the validity of the results match the expected result. + if (tokenValidationResult.IsValid != validationResult.IsValid) + { + context.AddDiff($"tokenValidationResult.IsValid != validationResult.IsSuccess"); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context); + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + } + else + { + if (tokenValidationResult.IsValid) + { + // Verify that the validated tokens from both paths match. + ValidatedToken validatedToken = validationResult.UnwrapResult(); + IdentityComparer.AreEqual(validatedToken.SecurityToken, tokenValidationResult.SecurityToken, context); + } + else + { + // Verify the exception provided by both paths match. + var tokenValidationResultException = tokenValidationResult.Exception; + theoryData.ExpectedException.ProcessException(tokenValidationResultException, context); + var validationResultException = validationResult.UnwrapError().GetException(); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResultException, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + } + + public static TheoryData ValidateTokenAsync_IssuerSigningKey_TestCases + { + get + { + int currentYear = DateTime.UtcNow.Year; + // Mock time provider, 100 years in the future + TimeProvider futureTimeProvider = new MockTimeProvider(new DateTimeOffset(currentYear + 100, 1, 1, 0, 0, 0, new(0))); + // Mock time provider, 100 years in the past + TimeProvider pastTimeProvider = new MockTimeProvider(new DateTimeOffset(currentYear - 100, 9, 16, 0, 0, 0, new(0))); + + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Valid_IssuerSigningKeyIsValid") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters(KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key), + ValidationParameters = CreateValidationParameters(KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_IssuerSigningKeyIsExpired") + { + // Signing key is valid between September 2011 and December 2039 + // Mock time provider is set to 100 years in the future, after the key expired + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, futureTimeProvider), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, futureTimeProvider), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10249:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10249:"), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_IssuerSigningKeyNotYetValid") + { + // Signing key is valid between September 2011 and December 2039 + // Mock time provider is set to 100 years in the past, before the key was valid. + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, pastTimeProvider), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, pastTimeProvider), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10248:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidSigningKeyException("IDX10248:"), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerSigningKeyTheoryData("Invalid_TokenValidationParametersAndValidationParametersAreNull") + { + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + ExpectedIsValid = false, + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters( + SecurityKey issuerSigingKey, TimeProvider? timeProvider = null) + { + ValidationParameters validationParameters = new ValidationParameters(); + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + validationParameters.TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + validationParameters.SignatureValidator = ( + SecurityToken token, + ValidationParameters validationParameters, + BaseConfiguration? configuration, + CallContext? callContext) => + { + // Set the signing key for validation + token.SigningKey = issuerSigingKey; + return issuerSigingKey; + }; + + if (issuerSigingKey is not null) + validationParameters.IssuerSigningKeys.Add(issuerSigingKey); + + if (timeProvider is not null) + validationParameters.TimeProvider = timeProvider; + + return validationParameters; + } + + static TokenValidationParameters CreateTokenValidationParameters( + SecurityKey? issuerSigningKey = null, TimeProvider? timeProvider = null) + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + RequireAudience = false, + IssuerSigningKey = issuerSigningKey, + }; + + tokenValidationParameters.SignatureValidator = (token, tokenValidationParameters) => + { + // Set the signing key for validation + SamlSecurityTokenHandler samlSecurityTokenHandler = new SamlSecurityTokenHandler(); + SamlSecurityToken samlSecurityToken = samlSecurityTokenHandler.ReadSamlToken(token); + samlSecurityToken.SigningKey = issuerSigningKey; + + return samlSecurityToken; + }; + + if (timeProvider is not null) + tokenValidationParameters.TimeProvider = timeProvider; + + return tokenValidationParameters; + } + } + } + + public class ValidateTokenAsyncIssuerSigningKeyTheoryData : TheoryDataBase + { + public ValidateTokenAsyncIssuerSigningKeyTheoryData(string testId) : base(testId) { } + + internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = ExpectedException.NoExceptionExpected; + + internal bool ExpectedIsValid { get; set; } = true; + + internal ValidationParameters? ValidationParameters { get; set; } + + internal TokenValidationParameters? TokenValidationParameters { get; set; } + + internal SigningCredentials? SigningCredentials { get; set; } + } + } +} +#nullable restore 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)