From aa986356a3848a0d1d7209f39ea663dc9f35c207 Mon Sep 17 00:00:00 2001 From: Filip Navara Date: Wed, 6 Nov 2024 20:56:29 +0100 Subject: [PATCH 1/4] Remove dependency on Microsoft.Bcl.TimeProvider for .NET 8+ targets (#2935) Co-authored-by: jennyf19 Co-authored-by: Peter <34331512+pmaytak@users.noreply.github.com> --- .../Microsoft.IdentityModel.JsonWebTokens.csproj | 2 +- .../Microsoft.IdentityModel.Tokens.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj b/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj index eef130efa1..bfab4b253e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Microsoft.IdentityModel.JsonWebTokens.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj index 5c0ac53091..459a772334 100644 --- a/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj +++ b/src/Microsoft.IdentityModel.Tokens/Microsoft.IdentityModel.Tokens.csproj @@ -57,7 +57,7 @@ - + From 73e2c18f2ddcd1bd41b94fc114d4312131fc73a5 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Thu, 7 Nov 2024 12:00:03 -0800 Subject: [PATCH 2/4] Update cgmanifest to align with the JSON schema (#2969) --- build/cgmanifest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/cgmanifest.json b/build/cgmanifest.json index fb683d916e..8e8d3a2779 100644 --- a/build/cgmanifest.json +++ b/build/cgmanifest.json @@ -1,5 +1,5 @@ { - "Registrations": [ + "registrations": [ { "component": { "type": "git", @@ -10,6 +10,6 @@ } } ], - "Version": 1, + "version": 1, "$schema": "https://json.schemastore.org/component-detection-manifest.json" -} \ No newline at end of file +} From a275ee4209a055caa5c485ebaae4c304b1cf273a Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Fri, 8 Nov 2024 18:36:00 +0000 Subject: [PATCH 3/4] SAML and SAML2 new model validation: Read Token (#2980) * Added ReadToken to SamlTokenHandler returning a ValidationResult. Added log messages for the malformed case, and SamlValidationError to return the correct exception. * Added ReadToken to Saml2TokenHandler returning a ValidationResult. Added log messages for the malformed case, and Saml2ValidationError to return the correct exception. * Addressed PR feedback --- .../InternalAPI.Unshipped.txt | 15 ++- .../Saml/Exceptions/SamlValidationError.cs | 26 ++++ .../Saml/LogMessages.cs | 1 + .../SamlSecurityTokenHandler.ReadToken.cs | 57 +++++++++ ...rityTokenHandler.ValidateToken.Internal.cs | 21 +++- .../Saml2/Exceptions/Saml2ValidationError.cs | 26 ++++ .../Saml2/LogMessages.cs | 1 + .../Saml2SecurityTokenHandler.ReadToken.cs | 59 ++++++++++ ...rityTokenHandler.ValidateToken.Internal.cs | 19 +++ .../InternalAPI.Unshipped.txt | 1 + .../Validation/ValidationFailureType.cs | 6 + .../ExpectedException.cs | 12 ++ ...aml2SecurityTokenHandlerTests.ReadToken.cs | 111 ++++++++++++++++++ ...SamlSecurityTokenHandlerTests.ReadToken.cs | 111 ++++++++++++++++++ 14 files changed, 463 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml/Exceptions/SamlValidationError.cs create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ReadToken.cs create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Exceptions/Saml2ValidationError.cs create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ReadToken.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ReadToken.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ReadToken.cs diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index 057dee7277..ec74043d3d 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -1,3 +1,5 @@ +const Microsoft.IdentityModel.Tokens.Saml.LogMessages.IDX11402 = "IDX11402: Unable to read SamlSecurityToken. Exception thrown: '{0}'." -> string +const Microsoft.IdentityModel.Tokens.Saml2.LogMessages.IDX13003 = "IDX13003: Unable to read Saml2SecurityToken. Exception thrown: '{0}'." -> string Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedAudience.get -> string @@ -7,13 +9,22 @@ Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedLifetime.get -> Microsoft.IdentityModel.Tokens.ValidatedLifetime? Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedLifetime.set -> void Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateTokenAsync(string token, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +Microsoft.IdentityModel.Tokens.Saml.SamlValidationError +Microsoft.IdentityModel.Tokens.Saml.SamlValidationError.SamlValidationError(Microsoft.IdentityModel.Tokens.MessageDetail MessageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType failureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame) -> void +Microsoft.IdentityModel.Tokens.Saml.SamlValidationError.SamlValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType failureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, System.Exception innerException) -> void Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateTokenAsync(string token, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +Microsoft.IdentityModel.Tokens.Saml2.Saml2ValidationError +Microsoft.IdentityModel.Tokens.Saml2.Saml2ValidationError.Saml2ValidationError(Microsoft.IdentityModel.Tokens.MessageDetail MessageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType failureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame) -> void +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.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 static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task -Microsoft.IdentityModel.Tokens.Saml2.SamlSecurityTokenHandler.ValidateTokenAsync(SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame @@ -32,4 +43,6 @@ static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.OneTimeUseValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.TokenNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.TokenValidationParametersNull -> System.Diagnostics.StackFrame +virtual Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ReadSamlToken(string token, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult virtual Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateConditions(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult +virtual Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ReadSaml2Token(string token, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/Exceptions/SamlValidationError.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/Exceptions/SamlValidationError.cs new file mode 100644 index 0000000000..48aff2a17d --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/Exceptions/SamlValidationError.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +namespace Microsoft.IdentityModel.Tokens.Saml +{ + internal class SamlValidationError : ValidationError + { + internal SamlValidationError(MessageDetail messageDetail, ValidationFailureType failureType, Type exceptionType, StackFrame stackFrame, Exception innerException) : base(messageDetail, failureType, exceptionType, stackFrame, innerException) + { + } + + internal override Exception GetException() + { + if (ExceptionType == typeof(SamlSecurityTokenReadException)) + { + var exception = new SamlSecurityTokenReadException(MessageDetail.Message, InnerException); + return exception; + } + + return base.GetException(); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs index 60814cdb2a..ad28653191 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs @@ -15,6 +15,7 @@ internal static class LogMessages // SecurityTokenHandler messages internal const string IDX11400 = "IDX11400: The '{0}', can only process SecurityTokens of type: '{1}'. The SecurityToken received is of type: '{2}'."; internal const string IDX11401 = "IDX11401: Unable to validate token. TokenValidationParameters.RequireAudience is true but no AudienceRestrictions were found in the inbound token."; + internal const string IDX11402 = "IDX11402: Unable to read SamlSecurityToken. Exception thrown: '{0}'."; // signature creation / validation internal const string IDX11312 = "IDX11312: Unable to validate token. A SamlSamlAttributeStatement can only have one SamlAttribute of type 'Actor'. This special SamlAttribute is used in delegation scenarios."; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ReadToken.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ReadToken.cs new file mode 100644 index 0000000000..f9c658aaf1 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ReadToken.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text; +using System.Xml; +using Microsoft.IdentityModel.Logging; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.Tokens.Saml +{ + public partial class SamlSecurityTokenHandler : SecurityTokenHandler + { + /// + /// Converts a string into an instance of . + /// + /// a Saml token as a string. + /// An opaque context used to store work when working with authentication artifacts. + /// A + /// If is null or empty. + /// If 'token.Length' is greater than . + internal virtual ValidationResult ReadSamlToken(string token, CallContext callContext) + { + if (string.IsNullOrEmpty(token)) + return ValidationError.NullParameter(nameof(token), ValidationError.GetCurrentStackFrame()); + + if (token.Length > MaximumTokenSizeInBytes) + return new ValidationError( + new MessageDetail( + TokenLogMessages.IDX10209, + LogHelper.MarkAsNonPII(token.Length), + LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)), + ValidationFailureType.TokenExceedsMaximumSize, + typeof(ArgumentOutOfRangeException), + ValidationError.GetCurrentStackFrame()); + + try + { + using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), XmlDictionaryReaderQuotas.Max)) + { + return ReadSamlToken(reader); + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new SamlValidationError( + new MessageDetail(LogMessages.IDX11402, ex.Message), + ValidationFailureType.TokenReadingFailed, + typeof(SamlSecurityTokenReadException), + ValidationError.GetCurrentStackFrame(), + ex); + } + } + } +} 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 04a2d3c92e..b5115646f3 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs @@ -14,9 +14,26 @@ namespace Microsoft.IdentityModel.Tokens.Saml /// public partial class SamlSecurityTokenHandler : SecurityTokenHandler { -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously internal async Task> ValidateTokenAsync( -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + string token, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + if (token is null) + return ValidationError.NullParameter(nameof(token), ValidationError.GetCurrentStackFrame()); + + if (validationParameters is null) + return ValidationError.NullParameter(nameof(validationParameters), ValidationError.GetCurrentStackFrame()); + + var tokenReadingResult = ReadSamlToken(token, callContext); + if (!tokenReadingResult.IsValid) + return tokenReadingResult.UnwrapError().AddCurrentStackFrame(); + + return await ValidateTokenAsync(tokenReadingResult.UnwrapResult(), validationParameters, callContext, cancellationToken).ConfigureAwait(false); + } + + internal async Task> ValidateTokenAsync( SamlSecurityToken samlToken, ValidationParameters validationParameters, CallContext callContext, diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Exceptions/Saml2ValidationError.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Exceptions/Saml2ValidationError.cs new file mode 100644 index 0000000000..b8f5803e23 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Exceptions/Saml2ValidationError.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +namespace Microsoft.IdentityModel.Tokens.Saml2 +{ + internal class Saml2ValidationError : ValidationError + { + internal Saml2ValidationError(MessageDetail messageDetail, ValidationFailureType failureType, Type exceptionType, StackFrame stackFrame, Exception innerException) : base(messageDetail, failureType, exceptionType, stackFrame, innerException) + { + } + + internal override Exception GetException() + { + if (ExceptionType == typeof(Saml2SecurityTokenReadException)) + { + var exception = new Saml2SecurityTokenReadException(MessageDetail.Message, InnerException); + return exception; + } + + return base.GetException(); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs index c440f6694f..a0e71cf811 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs @@ -16,6 +16,7 @@ internal static class LogMessages internal const string IDX13400 = "IDX13400: The '{0}', can only process SecurityTokens of type: '{1}'. The SecurityToken received is of type: '{2}'."; internal const string IDX13001 = "IDX13001: A SAML2 assertion that specifies an AuthenticationContext DeclarationReference is not supported.To handle DeclarationReference, extend the Saml2SecurityTokenHandler and override ProcessAuthenticationStatement."; internal const string IDX13002 = "IDX13002: Unable to validate token. TokenValidationParameters.RequireAudience is true but no AudienceRestrictions were found in the inbound token."; + internal const string IDX13003 = "IDX13003: Unable to read Saml2SecurityToken. Exception thrown: '{0}'."; // signature creation / validation internal const string IDX13509 = "IDX13509: Unable to validate token, Subject is null."; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ReadToken.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ReadToken.cs new file mode 100644 index 0000000000..cf74c5fc0b --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ReadToken.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text; +using System.Xml; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens.Saml; + +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.Tokens.Saml2 +{ + public partial class Saml2SecurityTokenHandler : SecurityTokenHandler + { + /// + /// Converts a string into an instance of . + /// + /// a Saml token as a string. + /// An opaque context used to store work when working with authentication artifacts. + /// A + /// If is null or empty. + /// If 'token.Length' is greater than . + internal virtual ValidationResult ReadSaml2Token(string token, CallContext callContext) + { + if (string.IsNullOrEmpty(token)) + return ValidationError.NullParameter(nameof(token), ValidationError.GetCurrentStackFrame()); + + if (token.Length > MaximumTokenSizeInBytes) + return new ValidationError( + new MessageDetail( + TokenLogMessages.IDX10209, + LogHelper.MarkAsNonPII(token.Length), + LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)), + ValidationFailureType.TokenReadingFailed, + typeof(ArgumentException), + ValidationError.GetCurrentStackFrame()); + + try + { + using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), XmlDictionaryReaderQuotas.Max)) + { + return ReadSaml2Token(reader); + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new Saml2ValidationError( + new MessageDetail(LogMessages.IDX13003, ex.Message), + ValidationFailureType.TokenReadingFailed, + typeof(Saml2SecurityTokenReadException), + ValidationError.GetCurrentStackFrame(), + ex); + } + } + } +} 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 0d23b4644f..5291cde721 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -15,6 +15,25 @@ namespace Microsoft.IdentityModel.Tokens.Saml2 /// public partial class Saml2SecurityTokenHandler : SecurityTokenHandler { + internal async Task> ValidateTokenAsync( + string token, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + if (token is null) + return ValidationError.NullParameter(nameof(token), ValidationError.GetCurrentStackFrame()); + + if (validationParameters is null) + return ValidationError.NullParameter(nameof(validationParameters), ValidationError.GetCurrentStackFrame()); + + var tokenReadingResult = ReadSaml2Token(token, callContext); + if (!tokenReadingResult.IsValid) + return tokenReadingResult.UnwrapError().AddCurrentStackFrame(); + + return await ValidateTokenAsync(tokenReadingResult.UnwrapResult(), validationParameters, callContext, cancellationToken).ConfigureAwait(false); + } + internal async Task> ValidateTokenAsync( Saml2SecurityToken samlToken, ValidationParameters validationParameters, diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 1d35266764..2ec92b086f 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -34,4 +34,5 @@ static Microsoft.IdentityModel.Tokens.ValidationError.GetCurrentStackFrame(strin 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 +static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.TokenExceedsMaximumSize -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.XmlValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 6cf24dc28c..00aba62c6e 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -99,6 +99,12 @@ private class TokenReplayValidationFailure : ValidationFailureType { internal To public static readonly ValidationFailureType TokenReadingFailed = new TokenReadingFailure("TokenReadingFailed"); private class TokenReadingFailure : ValidationFailureType { internal TokenReadingFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that a token exceeds the maximum size. + /// + public static readonly ValidationFailureType TokenExceedsMaximumSize = new TokenExceedsMaximumSizeFailure("TokenExceedsMaximumSize"); + private class TokenExceedsMaximumSizeFailure : ValidationFailureType { internal TokenExceedsMaximumSizeFailure(string name) : base(name) { } } + /// /// Defines a type that represents that a JWE could not be decrypted. /// diff --git a/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs b/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs index 366b357b88..6f8f717392 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs @@ -9,6 +9,8 @@ using System.Text.Json; using System.Xml; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Saml; +using Microsoft.IdentityModel.Tokens.Saml2; namespace Microsoft.IdentityModel.TestUtils { @@ -74,6 +76,16 @@ public static ExpectedException XmlException(string substringExpected = null, Ty return new ExpectedException(typeof(XmlException), substringExpected, inner); } + public static ExpectedException SamlSecurityTokenReadException(string substringExpected = null, Type inner = null, string contains = null) + { + return new ExpectedException(typeof(SamlSecurityTokenReadException), substringExpected, inner); + } + + public static ExpectedException Saml2SecurityTokenReadException(string substringExpected = null, Type inner = null, string contains = null) + { + return new ExpectedException(typeof(Saml2SecurityTokenReadException), substringExpected, inner); + } + public static ExpectedException NoExceptionExpected { get { return new ExpectedException(); } diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ReadToken.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ReadToken.cs new file mode 100644 index 0000000000..d51f1f0c3b --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ReadToken.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Xunit; +using Microsoft.IdentityModel.TestUtils; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens.Saml2.Tests +{ + public partial class Saml2SecurityTokenHandlerTests + { + [Theory, MemberData(nameof(ReadTokenTestCases), DisableDiscoveryEnumeration = true)] + public void ReadToken_ResultType(TokenReadingTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.ReadToken_ResultType", theoryData); + Saml2SecurityTokenHandler handler = new Saml2SecurityTokenHandler(); + ValidationResult result = handler.ReadSaml2Token( + theoryData.Token, + new CallContext()); + + if (result.IsValid) + { + IdentityComparer.AreEqual( + result.UnwrapResult(), + handler.ReadToken(theoryData.Token), + context); + + theoryData.ExpectedException.ProcessNoException(context); + } + else + { + ValidationError validationError = result.UnwrapError(); + IdentityComparer.AreStringsEqual( + validationError.FailureType.Name, + theoryData.Result.UnwrapError().FailureType.Name, + context); + + Exception exception = validationError.GetException(); + theoryData.ExpectedException.ProcessException(exception, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ReadTokenTestCases + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add(new TokenReadingTheoryData("Valid_SAML_Token") + { + Token = ReferenceTokens.Saml2Token_Valid, + }); + + theoryData.Add(new TokenReadingTheoryData("Invalid_NullToken") + { + Token = null, + ExpectedException = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + Result = new ValidationError( + new MessageDetail( + TokenLogMessages.IDX10000, + LogHelper.MarkAsNonPII("token")), + ValidationFailureType.NullArgument, + typeof(SecurityTokenArgumentNullException), + null) + }); + + theoryData.Add(new TokenReadingTheoryData("Invalid_EmptyToken") + { + Token = string.Empty, + ExpectedException = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + Result = new ValidationError( + new MessageDetail( + TokenLogMessages.IDX10000, + LogHelper.MarkAsNonPII("token")), + ValidationFailureType.NullArgument, + typeof(SecurityTokenArgumentNullException), + null) + }); + + theoryData.Add(new TokenReadingTheoryData("Invalid_MalformedToken") + { + Token = ReferenceTokens.Saml2Token_MissingVersion, + ExpectedException = ExpectedException.Saml2SecurityTokenReadException("IDX13003:", inner: typeof(Saml2SecurityTokenReadException)), + Result = new ValidationError( + new MessageDetail(LogMessages.IDX13003, "exception message"), + ValidationFailureType.TokenReadingFailed, + typeof(Saml2SecurityTokenReadException), + null), + }); + + return theoryData; + } + } + } + + public class TokenReadingTheoryData : TheoryDataBase + { + public TokenReadingTheoryData(string testId) + { + TestId = testId; + } + + public string Token { get; set; } + + internal ValidationResult Result { get; set; } + } +} diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ReadToken.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ReadToken.cs new file mode 100644 index 0000000000..a62c5c4938 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ReadToken.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Xunit; +using Microsoft.IdentityModel.TestUtils; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens.Saml.Tests +{ + public partial class SamlSecurityTokenHandlerTests + { + [Theory, MemberData(nameof(ReadTokenTestCases), DisableDiscoveryEnumeration = true)] + public void ReadToken_ResultType(TokenReadingTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.ReadToken_ResultType", theoryData); + SamlSecurityTokenHandler handler = new SamlSecurityTokenHandler(); + ValidationResult result = handler.ReadSamlToken( + theoryData.Token, + new CallContext()); + + if (result.IsValid) + { + IdentityComparer.AreEqual( + result.UnwrapResult(), + handler.ReadToken(theoryData.Token), + context); + + theoryData.ExpectedException.ProcessNoException(context); + } + else + { + ValidationError validationError = result.UnwrapError(); + IdentityComparer.AreStringsEqual( + validationError.FailureType.Name, + theoryData.Result.UnwrapError().FailureType.Name, + context); + + Exception exception = validationError.GetException(); + theoryData.ExpectedException.ProcessException(exception, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ReadTokenTestCases + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add(new TokenReadingTheoryData("Valid_SAML_Token") + { + Token = ReferenceTokens.SamlToken_Valid, + }); + + theoryData.Add(new TokenReadingTheoryData("Invalid_NullToken") + { + Token = null, + ExpectedException = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + Result = new ValidationError( + new MessageDetail( + TokenLogMessages.IDX10000, + LogHelper.MarkAsNonPII("token")), + ValidationFailureType.NullArgument, + typeof(SecurityTokenArgumentNullException), + null) + }); + + theoryData.Add(new TokenReadingTheoryData("Invalid_EmptyToken") + { + Token = string.Empty, + ExpectedException = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + Result = new ValidationError( + new MessageDetail( + TokenLogMessages.IDX10000, + LogHelper.MarkAsNonPII("token")), + ValidationFailureType.NullArgument, + typeof(SecurityTokenArgumentNullException), + null) + }); + + theoryData.Add(new TokenReadingTheoryData("Invalid_MalformedToken") + { + Token = ReferenceTokens.SamlToken_MissingMajorVersion, + ExpectedException = ExpectedException.SamlSecurityTokenReadException("IDX11402:", inner: typeof(SamlSecurityTokenReadException)), + Result = new ValidationError( + new MessageDetail(LogMessages.IDX11402, "exception message"), + ValidationFailureType.TokenReadingFailed, + typeof(SamlSecurityTokenReadException), + null), + }); + + return theoryData; + } + } + } + + public class TokenReadingTheoryData : TheoryDataBase + { + public TokenReadingTheoryData(string testId) + { + TestId = testId; + } + + public string Token { get; set; } + + internal ValidationResult Result { get; set; } + } +} From dfae4af4b83ce0bcdf41abd7063a5f7840c0e5e7 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Fri, 8 Nov 2024 19:40:29 +0000 Subject: [PATCH 4/4] SAML2 new model validation: Signature (#2961) * Added XmlValidationError. Added ValidationError property to XmlValidationException to provide custom stack traces * Added alternative versions using ValidationParameters to XML signature validations * Added XmlValidationFailure to ValidationFailureType * Added refactored ValidateSignature method to SamlSecurityTokenHandler. Updated ValidateTokenAsync to call ValidateSignature. * Added tests to compare signature validation between the legacy and new path * Re-added API lost in merge to InternalAPI.Unshipped.txt * Migrated refactored ValidateSignature from SamlSecurityTokenHandler to Saml2SecurityTokenHandler * Updated Saml2SecurityTokenHandler's ValidateTokenAsync to validate signatures * Added tests * Update src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs Co-authored-by: msbw2 * Addressed PR feedback in both SAML and SAML2 signature validations to keep the alignment * Optimised signature validation in SAML and SAML2 for the expected most common scenario * Removed debug information * Addressed PR feedback * Addressed PR feedback --------- Co-authored-by: msbw2 --- .../InternalAPI.Unshipped.txt | 4 +- ...lSecurityTokenHandler.ValidateSignature.cs | 144 +++++++----- ...2SecurityTokenHandler.ValidateSignature.cs | 196 +++++++++++++++++ ...rityTokenHandler.ValidateToken.Internal.cs | 7 + ...yTokenHandler.ValidateToken.StackFrames.cs | 3 +- .../InternalAPI.Unshipped.txt | 1 + .../Validation/ValidationFailureType.cs | 6 + src/Microsoft.IdentityModel.Xml/Signature.cs | 15 +- src/Microsoft.IdentityModel.Xml/SignedInfo.cs | 4 +- ...Tests.ValidateTokenAsyncTests.Signature.cs | 207 ++++++++++++++++++ 10 files changed, 518 insertions(+), 69 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index ec74043d3d..748018b3ab 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -41,8 +41,10 @@ static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.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 +static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.SignatureValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.TokenNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.TokenValidationParametersNull -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateSignature(Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult +virtual Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ReadSaml2Token(string token, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult virtual Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ReadSamlToken(string token, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult virtual Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateConditions(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult -virtual Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ReadSaml2Token(string token, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs index 99e50582c3..3691a62159 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.Diagnostics; using System.Text; using Microsoft.IdentityModel.Xml; using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; @@ -23,14 +22,14 @@ internal static ValidationResult ValidateSignature( { return ValidationError.NullParameter( nameof(samlToken), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); } if (validationParameters is null) { return ValidationError.NullParameter( nameof(validationParameters), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); } // Delegate is set by the user, we call it and return the result. @@ -43,11 +42,10 @@ internal static ValidationResult ValidateSignature( new MessageDetail( TokenLogMessages.IDX10504, samlToken.Assertion.CanonicalString), - ValidationFailureType.SignatureValidationFailed, + ValidationFailureType.TokenIsNotSigned, typeof(SecurityTokenValidationException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); - IList? keys = null; SecurityKey? resolvedKey = null; bool keyMatched = false; @@ -66,57 +64,39 @@ internal static ValidationResult ValidateSignature( resolvedKey = SamlTokenUtilities.ResolveTokenSigningKey(samlToken.Assertion.Signature.KeyInfo, validationParameters); } - if (resolvedKey is null) - { - if (validationParameters.TryAllIssuerSigningKeys) - keys = validationParameters.IssuerSigningKeys; - } - else + ValidationError? error = null; + + if (resolvedKey is not null) { - keys = [resolvedKey]; keyMatched = true; + var result = ValidateSignatureUsingKey(resolvedKey, samlToken, validationParameters, callContext); + if (result.IsValid) + return result; + + error = result.UnwrapError(); } bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; - List errors = new(); - StringBuilder keysAttempted = new(); + List? errors = null; + StringBuilder? keysAttempted = null; - if (keys is not null) + if (!keyMatched && validationParameters.TryAllIssuerSigningKeys && validationParameters.IssuerSigningKeys is not null) { - for (int i = 0; i < keys.Count; i++) + // Control reaches here only if the key could not be resolved and TryAllIssuerSigningKeys is set to true. + // We try all the keys in the list and return the first valid key. This is the degenerate case. + for (int i = 0; i < validationParameters.IssuerSigningKeys.Count; i++) { - SecurityKey key = keys[i]; - ValidationResult algorithmValidationResult = validationParameters.AlgorithmValidator( - samlToken.Assertion.Signature.SignedInfo.SignatureMethod, - key, - samlToken, - validationParameters, - callContext); + SecurityKey key = validationParameters.IssuerSigningKeys[i]; + if (key is null) + continue; - if (!algorithmValidationResult.IsValid) - { - errors.Add(algorithmValidationResult.UnwrapError()); - } - else - { - var validationError = samlToken.Assertion.Signature.Verify( - key, - validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory, - callContext); + var result = ValidateSignatureUsingKey(key, samlToken, validationParameters, callContext); + if (result.IsValid) + return result; - if (validationError is null) - { - samlToken.SigningKey = key; + (errors ??= new()).Add(result.UnwrapError()); - return key; - } - else - { - errors.Add(validationError.AddStackFrame(new StackFrame())); - } - } - - keysAttempted.Append(key.ToString()); + (keysAttempted ??= new()).Append(key.ToString()); if (canMatchKey && !keyMatched && key.KeyId is not null && samlToken.Assertion.Signature.KeyInfo is not null) keyMatched = samlToken.Assertion.Signature.KeyInfo.MatchesKey(key); } @@ -126,38 +106,88 @@ internal static ValidationResult ValidateSignature( return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10514, - keysAttempted.ToString(), + keysAttempted?.ToString(), samlToken.Assertion.Signature.KeyInfo, - GetErrorStrings(errors), + GetErrorString(error, errors), samlToken), ValidationFailureType.SignatureValidationFailed, typeof(SecurityTokenInvalidSignatureException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); + + string? keysAttemptedString = null; + if (resolvedKey is not null) + keysAttemptedString = resolvedKey.ToString(); + else if ((keysAttempted?.Length ?? 0) > 0) + keysAttemptedString = keysAttempted!.ToString(); - if (keysAttempted.Length > 0) + if (keysAttemptedString is not null) return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10512, - keysAttempted.ToString(), - GetErrorStrings(errors), + keysAttemptedString, + GetErrorString(error, errors), samlToken), ValidationFailureType.SignatureValidationFailed, typeof(SecurityTokenSignatureKeyNotFoundException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); return new XmlValidationError( new MessageDetail(TokenLogMessages.IDX10500), ValidationFailureType.SignatureValidationFailed, typeof(SecurityTokenSignatureKeyNotFoundException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); } - private static string GetErrorStrings(List errors) + private static ValidationResult ValidateSignatureUsingKey(SecurityKey key, SamlSecurityToken samlToken, ValidationParameters validationParameters, CallContext callContext) { + ValidationResult algorithmValidationResult = validationParameters.AlgorithmValidator( + samlToken.Assertion.Signature.SignedInfo.SignatureMethod, + key, + samlToken, + validationParameters, + callContext); + + if (!algorithmValidationResult.IsValid) + { + return algorithmValidationResult.UnwrapError().AddCurrentStackFrame(); + } + else + { + var validationError = samlToken.Assertion.Signature.Verify( + key, + validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory, + callContext); + + if (validationError is null) + { + samlToken.SigningKey = key; + + return key; + } + else + { + return validationError.AddCurrentStackFrame(); + } + } + } + + private static string GetErrorString(ValidationError? error, List? errorList) + { + // This method is called if there are errors in the signature validation process. + // This check is there to account for the optional parameter. + if (error is not null) + return error.MessageDetail.Message; + + if (errorList is null) + return string.Empty; + + if (errorList.Count == 1) + return errorList[0].MessageDetail.Message; + StringBuilder sb = new(); - for (int i = 0; i < errors.Count; i++) + for (int i = 0; i < errorList.Count; i++) { - sb.AppendLine(errors[i].ToString()); + sb.AppendLine(errorList[i].MessageDetail.Message); } return sb.ToString(); diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs new file mode 100644 index 0000000000..74d2884b26 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text; +using Microsoft.IdentityModel.Tokens.Saml; +using Microsoft.IdentityModel.Xml; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml2 +{ + public partial class Saml2SecurityTokenHandler : SecurityTokenHandler + { + internal static ValidationResult ValidateSignature( + Saml2SecurityToken samlToken, + ValidationParameters validationParameters, + CallContext callContext) + { + if (samlToken is null) + { + return ValidationError.NullParameter( + nameof(samlToken), + ValidationError.GetCurrentStackFrame()); + } + + if (validationParameters is null) + { + return ValidationError.NullParameter( + nameof(validationParameters), + ValidationError.GetCurrentStackFrame()); + } + + // Delegate is set by the user, we call it and return the result. + if (validationParameters.SignatureValidator is not null) + return validationParameters.SignatureValidator(samlToken, validationParameters, null, callContext); + + // If the user wants to accept unsigned tokens, they must set validationParameters.SignatureValidator + if (samlToken.Assertion.Signature is null) + return new XmlValidationError( + new MessageDetail( + TokenLogMessages.IDX10504, + samlToken.Assertion.CanonicalString), + ValidationFailureType.TokenIsNotSigned, + typeof(SecurityTokenValidationException), + ValidationError.GetCurrentStackFrame()); + + SecurityKey? resolvedKey = null; + bool keyMatched = false; + + if (validationParameters.IssuerSigningKeyResolver is not null) + { + resolvedKey = validationParameters.IssuerSigningKeyResolver( + samlToken.Assertion.CanonicalString, + samlToken, + samlToken.Assertion.Signature.KeyInfo?.Id, + validationParameters, + null, + callContext); + } + else + { + resolvedKey = SamlTokenUtilities.ResolveTokenSigningKey(samlToken.Assertion.Signature.KeyInfo, validationParameters); + } + + ValidationError? error = null; + + if (resolvedKey is not null) + { + keyMatched = true; + var result = ValidateSignatureUsingKey(resolvedKey, samlToken, validationParameters, callContext); + if (result.IsValid) + return result; + + error = result.UnwrapError(); + } + + bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; + List? errors = null; + StringBuilder? keysAttempted = null; + + if (!keyMatched && validationParameters.TryAllIssuerSigningKeys && validationParameters.IssuerSigningKeys is not null) + { + // Control reaches here only if the key could not be resolved and TryAllIssuerSigningKeys is set to true. + // We try all the keys in the list and return the first valid key. This is the degenerate case. + for (int i = 0; i < validationParameters.IssuerSigningKeys.Count; i++) + { + SecurityKey key = validationParameters.IssuerSigningKeys[i]; + if (key is null) + continue; + + var result = ValidateSignatureUsingKey(key, samlToken, validationParameters, callContext); + if (result.IsValid) + return result; + + (errors ??= new()).Add(result.UnwrapError()); + + (keysAttempted ??= new()).Append(key.ToString()); + if (canMatchKey && !keyMatched && key.KeyId is not null && samlToken.Assertion.Signature.KeyInfo is not null) + keyMatched = samlToken.Assertion.Signature.KeyInfo.MatchesKey(key); + } + } + + if (canMatchKey && keyMatched) + return new XmlValidationError( + new MessageDetail( + TokenLogMessages.IDX10514, + keysAttempted?.ToString(), + samlToken.Assertion.Signature.KeyInfo, + GetErrorStrings(error, errors), + samlToken), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenInvalidSignatureException), + ValidationError.GetCurrentStackFrame()); + + string? keysAttemptedString = null; + if (resolvedKey is not null) + keysAttemptedString = resolvedKey.ToString(); + else if ((keysAttempted?.Length ?? 0) > 0) + keysAttemptedString = keysAttempted!.ToString(); + + if (keysAttemptedString is not null) + return new XmlValidationError( + new MessageDetail( + TokenLogMessages.IDX10512, + keysAttemptedString, + GetErrorStrings(error, errors), + samlToken), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenSignatureKeyNotFoundException), + ValidationError.GetCurrentStackFrame()); + + return new XmlValidationError( + new MessageDetail(TokenLogMessages.IDX10500), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenSignatureKeyNotFoundException), + ValidationError.GetCurrentStackFrame()); + } + + private static ValidationResult ValidateSignatureUsingKey(SecurityKey key, Saml2SecurityToken samlToken, ValidationParameters validationParameters, CallContext callContext) + { + ValidationResult algorithmValidationResult = validationParameters.AlgorithmValidator( + samlToken.Assertion.Signature.SignedInfo.SignatureMethod, + key, + samlToken, + validationParameters, + callContext); + + if (!algorithmValidationResult.IsValid) + { + return algorithmValidationResult.UnwrapError().AddCurrentStackFrame(); + } + else + { + var validationError = samlToken.Assertion.Signature.Verify( + key, + validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory, + callContext); + + if (validationError is null) + { + samlToken.SigningKey = key; + + return key; + } + else + { + return validationError.AddCurrentStackFrame(); + } + } + } + + private static string GetErrorStrings(ValidationError? error, List? errors) + { + // This method is called if there are errors in the signature validation process. + // This check is there to account for the optional parameter. + if (error is not null) + return error.MessageDetail.Message; + + if (errors is null) + return string.Empty; + + if (errors.Count == 1) + return errors[0].MessageDetail.Message; + + StringBuilder sb = new(); + for (int i = 0; i < errors.Count; i++) + { + sb.AppendLine(errors[i].MessageDetail.Message); + } + + return sb.ToString(); + } + } +} +#nullable restore 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 5291cde721..d806f6a453 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -82,6 +82,13 @@ internal async Task> ValidateTokenAsync( return validatedIssuerResult.UnwrapError().AddStackFrame(StackFrames.IssuerValidationFailed); } + var signatureValidationResult = ValidateSignature(samlToken, validationParameters, callContext); + if (!signatureValidationResult.IsValid) + { + StackFrames.SignatureValidationFailed ??= new StackFrame(true); + return signatureValidationResult.UnwrapError().AddStackFrame(StackFrames.SignatureValidationFailed); + } + 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 dcd253940a..5638bf2f11 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs @@ -14,12 +14,13 @@ 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? SignatureValidationFailed; // Stack frames from ValidateConditions internal static StackFrame? AudienceValidationFailed; internal static StackFrame? AssertionNull; internal static StackFrame? AssertionConditionsNull; - internal static StackFrame? IssuerValidationFailed; internal static StackFrame? AssertionConditionsValidationFailed; internal static StackFrame? LifetimeValidationFailed; internal static StackFrame? OneTimeUseValidationFailed; diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 2ec92b086f..e7eaf3cd7f 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -35,4 +35,5 @@ static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoTokenAudi static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoValidationParameterAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureAlgorithmValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.TokenExceedsMaximumSize -> Microsoft.IdentityModel.Tokens.ValidationFailureType +static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.TokenIsNotSigned -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.XmlValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 00aba62c6e..cd58d536c7 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -69,6 +69,12 @@ private class TokenTypeValidationFailure : ValidationFailureType { internal Toke public static readonly ValidationFailureType SignatureValidationFailed = new SignatureValidationFailure("SignatureValidationFailed"); private class SignatureValidationFailure : ValidationFailureType { internal SignatureValidationFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that the token is not signed. + /// + public static readonly ValidationFailureType TokenIsNotSigned = new TokenNotSignedFailure("TokenIsNotSigned"); + private class TokenNotSignedFailure : ValidationFailureType { internal TokenNotSignedFailure(string name) : base(name) { } } + /// /// Defines a type that represents that the token's signature algorithm validation failed. /// diff --git a/src/Microsoft.IdentityModel.Xml/Signature.cs b/src/Microsoft.IdentityModel.Xml/Signature.cs index 8c36911800..b7bf6219fa 100644 --- a/src/Microsoft.IdentityModel.Xml/Signature.cs +++ b/src/Microsoft.IdentityModel.Xml/Signature.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Diagnostics; using System.IO; using Microsoft.IdentityModel.Tokens; using static Microsoft.IdentityModel.Logging.LogHelper; @@ -135,24 +134,24 @@ public void Verify(SecurityKey key, CryptoProviderFactory cryptoProviderFactory) #pragma warning restore CA1801 { if (key is null) - return ValidationError.NullParameter(nameof(key), new StackFrame()); + return ValidationError.NullParameter(nameof(key), ValidationError.GetCurrentStackFrame()); if (cryptoProviderFactory is null) - return ValidationError.NullParameter(nameof(cryptoProviderFactory), new StackFrame()); + return ValidationError.NullParameter(nameof(cryptoProviderFactory), ValidationError.GetCurrentStackFrame()); if (SignedInfo is null) return new XmlValidationError( new MessageDetail(LogMessages.IDX30212), ValidationFailureType.XmlValidationFailed, typeof(XmlValidationException), - new StackFrame()); + ValidationError.GetCurrentStackFrame()); if (!cryptoProviderFactory.IsSupportedAlgorithm(SignedInfo.SignatureMethod, key)) return new XmlValidationError( new MessageDetail(LogMessages.IDX30207, SignedInfo.SignatureMethod, cryptoProviderFactory.GetType()), ValidationFailureType.XmlValidationFailed, typeof(XmlValidationException), - new StackFrame()); + ValidationError.GetCurrentStackFrame()); var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, SignedInfo.SignatureMethod); if (signatureProvider is null) @@ -160,7 +159,7 @@ public void Verify(SecurityKey key, CryptoProviderFactory cryptoProviderFactory) new MessageDetail(LogMessages.IDX30203, cryptoProviderFactory, key, SignedInfo.SignatureMethod), ValidationFailureType.XmlValidationFailed, typeof(XmlValidationException), - new StackFrame()); + ValidationError.GetCurrentStackFrame()); ValidationError? validationError = null; @@ -175,14 +174,14 @@ public void Verify(SecurityKey key, CryptoProviderFactory cryptoProviderFactory) new MessageDetail(LogMessages.IDX30200, cryptoProviderFactory, key), ValidationFailureType.XmlValidationFailed, typeof(XmlValidationException), - new StackFrame()); + ValidationError.GetCurrentStackFrame()); } } if (validationError is null) { validationError = SignedInfo.Verify(cryptoProviderFactory, callContext); - validationError?.AddStackFrame(new StackFrame()); + validationError?.AddCurrentStackFrame(); } } finally diff --git a/src/Microsoft.IdentityModel.Xml/SignedInfo.cs b/src/Microsoft.IdentityModel.Xml/SignedInfo.cs index 7f18cdad4b..9f16187e56 100644 --- a/src/Microsoft.IdentityModel.Xml/SignedInfo.cs +++ b/src/Microsoft.IdentityModel.Xml/SignedInfo.cs @@ -125,7 +125,7 @@ public void Verify(CryptoProviderFactory cryptoProviderFactory) #pragma warning restore CA1801 { if (cryptoProviderFactory == null) - return ValidationError.NullParameter(nameof(cryptoProviderFactory), new System.Diagnostics.StackFrame()); + return ValidationError.NullParameter(nameof(cryptoProviderFactory), ValidationError.GetCurrentStackFrame()); ValidationError? validationError = null; @@ -136,7 +136,7 @@ public void Verify(CryptoProviderFactory cryptoProviderFactory) if (validationError is not null) { - validationError.AddStackFrame(new System.Diagnostics.StackFrame()); + validationError.AddCurrentStackFrame(); break; } } diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs new file mode 100644 index 0000000000..dbe28f7582 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Saml2; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Saml.Tests +{ +#nullable enable + public partial class Saml2SecurityTokenHandlerTests + { + [Theory, MemberData(nameof(ValidateTokenAsync_Signature_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_SignatureComparison(ValidateTokenAsyncSignatureTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_SignatureComparison", theoryData); + + Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); + + Saml2SecurityToken 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; + var validationResultException = validationResult.UnwrapError().GetException(); + + theoryData.ExpectedException.ProcessException(tokenValidationResultException, context); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResultException, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + } + + public static TheoryData ValidateTokenAsync_Signature_TestCases + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncSignatureTheoryData("Valid_SignatureIsValid") + { + 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 ValidateTokenAsyncSignatureTheoryData("Invalid_TokenIsNotSigned") + { + SigningCredentials = null, + TokenValidationParameters = CreateTokenValidationParameters(), + ValidationParameters = CreateValidationParameters(), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenValidationException("IDX10504:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenValidationException("IDX10504:"), + }); + + theoryData.Add(new ValidateTokenAsyncSignatureTheoryData("Invalid_TokenSignedWithDifferentKey_KeyIdPresent_TryAllKeysFalse") + { + SigningCredentials = Default.SymmetricSigningCredentials, + TokenValidationParameters = CreateTokenValidationParameters(Default.AsymmetricSigningKey), + ValidationParameters = CreateValidationParameters(Default.AsymmetricSigningKey), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + }); + + theoryData.Add(new ValidateTokenAsyncSignatureTheoryData("Invalid_TokenSignedWithDifferentKey_KeyIdPresent_TryAllKeysTrue") + { + SigningCredentials = Default.SymmetricSigningCredentials, + TokenValidationParameters = CreateTokenValidationParameters(Default.AsymmetricSigningKey, tryAllKeys: true), + ValidationParameters = CreateValidationParameters(Default.AsymmetricSigningKey, tryAllKeys: true), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + }); + + theoryData.Add(new ValidateTokenAsyncSignatureTheoryData("Invalid_TokenSignedWithDifferentKey_KeyIdNotPresent_TryAllKeysFalse") + { + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId, + TokenValidationParameters = CreateTokenValidationParameters(Default.AsymmetricSigningKey), + ValidationParameters = CreateValidationParameters(Default.AsymmetricSigningKey), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + }); + + theoryData.Add(new ValidateTokenAsyncSignatureTheoryData("Invalid_TokenSignedWithDifferentKey_KeyIdNotPresent_TryAllKeysTrue") + { + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2_NoKeyId, + TokenValidationParameters = CreateTokenValidationParameters(Default.AsymmetricSigningKey, tryAllKeys: true), + ValidationParameters = CreateValidationParameters(Default.AsymmetricSigningKey, tryAllKeys: true), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + }); + + theoryData.Add(new ValidateTokenAsyncSignatureTheoryData("Invalid_TokenValidationParametersAndValidationParametersAreNull") + { + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + ExpectedIsValid = false, + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters( + SecurityKey? issuerSigingKey = null, bool tryAllKeys = false) + { + ValidationParameters validationParameters = new ValidationParameters(); + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation; + validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + validationParameters.TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + validationParameters.TryAllIssuerSigningKeys = tryAllKeys; + + if (issuerSigingKey is not null) + validationParameters.IssuerSigningKeys.Add(issuerSigingKey); + + return validationParameters; + } + + static TokenValidationParameters CreateTokenValidationParameters( + SecurityKey? issuerSigningKey = null, bool tryAllKeys = false) + { + return new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = false, + RequireSignedTokens = true, + RequireAudience = false, + IssuerSigningKey = issuerSigningKey, + TryAllIssuerSigningKeys = tryAllKeys, + }; + } + } + } + + public class ValidateTokenAsyncSignatureTheoryData : TheoryDataBase + { + public ValidateTokenAsyncSignatureTheoryData(string testId) : base(testId) { } + + internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = ExpectedException.NoExceptionExpected; + + internal SigningCredentials? SigningCredentials { get; set; } = null; + + internal bool ExpectedIsValid { get; set; } = true; + + internal ValidationParameters? ValidationParameters { get; set; } + + internal TokenValidationParameters? TokenValidationParameters { get; set; } + } + + private static Saml2SecurityToken CreateTokenForSignatureValidation(SigningCredentials? signingCredentials) + { + Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + Subject = Default.SamlClaimsIdentity, + SigningCredentials = signingCredentials, + Issuer = Default.Issuer, + }; + + Saml2SecurityToken samlToken = (Saml2SecurityToken)saml2TokenHandler.CreateToken(securityTokenDescriptor); + + return saml2TokenHandler.ReadSaml2Token(samlToken.Assertion.CanonicalString); + } + } +} +#nullable restore