From f92bafe5756d5979c73fda04ef25773d228100a4 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 31 Oct 2024 16:07:05 +0000 Subject: [PATCH 01/14] Added XmlValidationError. Added ValidationError property to XmlValidationException to provide custom stack traces --- .../Exceptions/XmlValidationError.cs | 34 +++++++++++++ .../Exceptions/XmlValidationException.cs | 48 +++++++++++++++++++ .../InternalAPI.Unshipped.txt | 4 ++ .../PublicAPI.Unshipped.txt | 1 + 4 files changed, 87 insertions(+) create mode 100644 src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationError.cs diff --git a/src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationError.cs b/src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationError.cs new file mode 100644 index 0000000000..6374d3edb8 --- /dev/null +++ b/src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationError.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.IdentityModel.Xml +{ + internal class XmlValidationError : ValidationError + { + public XmlValidationError( + MessageDetail messageDetail, + ValidationFailureType validationFailureType, + Type exceptionType, + StackFrame stackFrame) : + base(messageDetail, validationFailureType, exceptionType, stackFrame) + { + + } + + internal override Exception GetException() + { + if (ExceptionType == typeof(XmlValidationException)) + { + XmlValidationException exception = new(MessageDetail.Message, InnerException); + exception.SetValidationError(this); + return exception; + } + + return base.GetException(); + } + } +} diff --git a/src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationException.cs b/src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationException.cs index 4e26c2f44c..702ff103bc 100644 --- a/src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationException.cs +++ b/src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationException.cs @@ -2,7 +2,12 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.Runtime.Serialization; +#pragma warning disable IDE0005 // Using directive is unnecessary. +using System.Text; +#pragma warning restore IDE0005 // Using directive is unnecessary. +using Microsoft.IdentityModel.Tokens; namespace Microsoft.IdentityModel.Xml { @@ -12,6 +17,11 @@ namespace Microsoft.IdentityModel.Xml [Serializable] public class XmlValidationException : XmlException { + [NonSerialized] + private string _stackTrace; + + private ValidationError _validationError; + /// /// Initializes a new instance of the class. /// @@ -49,5 +59,43 @@ protected XmlValidationException(SerializationInfo info, StreamingContext contex : base(info, context) { } + + /// + /// Sets the that caused the exception. + /// + /// + internal void SetValidationError(ValidationError validationError) + { + _validationError = validationError; + } + + /// + /// Gets the stack trace that is captured when the exception is created. + /// + public override string StackTrace + { + get + { + if (_stackTrace == null) + { + if (_validationError == null) + return base.StackTrace; +#if NET8_0_OR_GREATER + _stackTrace = new StackTrace(_validationError.StackFrames).ToString(); +#else + StringBuilder sb = new(); + foreach (StackFrame frame in _validationError.StackFrames) + { + sb.Append(frame.ToString()); + sb.Append(Environment.NewLine); + } + + _stackTrace = sb.ToString(); +#endif + } + + return _stackTrace; + } + } } } diff --git a/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt index e69de29bb2..8947ca3748 100644 --- a/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt @@ -0,0 +1,4 @@ +Microsoft.IdentityModel.Xml.XmlValidationError +Microsoft.IdentityModel.Xml.XmlValidationError.XmlValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame) -> void +Microsoft.IdentityModel.Xml.XmlValidationException.SetValidationError(Microsoft.IdentityModel.Tokens.ValidationError validationError) -> void +override Microsoft.IdentityModel.Xml.XmlValidationError.GetException() -> System.Exception \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Xml/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Xml/PublicAPI.Unshipped.txt index e69de29bb2..2d773dfa55 100644 --- a/src/Microsoft.IdentityModel.Xml/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Xml/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +override Microsoft.IdentityModel.Xml.XmlValidationException.StackTrace.get -> string From b141a1a114605b45941ca7f4d0e79148b3140cee Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 31 Oct 2024 16:08:08 +0000 Subject: [PATCH 02/14] Added alternative versions using ValidationParameters to XML signature validations --- .../InternalAPI.Unshipped.txt | 3 + src/Microsoft.IdentityModel.Xml/Reference.cs | 32 +++++++- src/Microsoft.IdentityModel.Xml/Signature.cs | 73 +++++++++++++++++++ src/Microsoft.IdentityModel.Xml/SignedInfo.cs | 33 +++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt index 8947ca3748..fb30836a8b 100644 --- a/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt @@ -1,3 +1,6 @@ +Microsoft.IdentityModel.Xml.Reference.Verify(Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError +Microsoft.IdentityModel.Xml.Signature.Verify(Microsoft.IdentityModel.Tokens.SecurityKey key, Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError +Microsoft.IdentityModel.Xml.SignedInfo.Verify(Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError Microsoft.IdentityModel.Xml.XmlValidationError Microsoft.IdentityModel.Xml.XmlValidationError.XmlValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame) -> void Microsoft.IdentityModel.Xml.XmlValidationException.SetValidationError(Microsoft.IdentityModel.Tokens.ValidationError validationError) -> void diff --git a/src/Microsoft.IdentityModel.Xml/Reference.cs b/src/Microsoft.IdentityModel.Xml/Reference.cs index dcae13c9b2..2a1f6870af 100644 --- a/src/Microsoft.IdentityModel.Xml/Reference.cs +++ b/src/Microsoft.IdentityModel.Xml/Reference.cs @@ -126,6 +126,36 @@ public void Verify(CryptoProviderFactory cryptoProviderFactory) throw LogValidationException(LogMessages.IDX30201, Uri ?? Id); } +#nullable enable + /// + /// Verifies that the equals the hashed value of the after + /// have been applied. + /// + /// supplies the . + /// contextual information for diagnostics. + /// if is null. + internal ValidationError? Verify( + CryptoProviderFactory cryptoProviderFactory, +#pragma warning disable CA1801 // Review unused parameters + CallContext callContext) +#pragma warning restore CA1801 + { + if (cryptoProviderFactory == null) + return ValidationError.NullParameter(nameof(cryptoProviderFactory), new System.Diagnostics.StackFrame()); + + if (!Utility.AreEqual(ComputeDigest(cryptoProviderFactory), Convert.FromBase64String(DigestValue))) + return new XmlValidationError( + new MessageDetail( + LogMessages.IDX30201, + Uri ?? Id), + ValidationFailureType.XmlValidationFailed, + typeof(XmlValidationException), + new System.Diagnostics.StackFrame()); + + return null; + } +#nullable restore + /// /// Writes into a stream and then hashes the bytes. /// @@ -194,4 +224,4 @@ protected byte[] ComputeDigest(CryptoProviderFactory cryptoProviderFactory) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.IdentityModel.Xml/Signature.cs b/src/Microsoft.IdentityModel.Xml/Signature.cs index 717422ca87..8c36911800 100644 --- a/src/Microsoft.IdentityModel.Xml/Signature.cs +++ b/src/Microsoft.IdentityModel.Xml/Signature.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.IO; using Microsoft.IdentityModel.Tokens; using static Microsoft.IdentityModel.Logging.LogHelper; @@ -124,5 +125,77 @@ public void Verify(SecurityKey key, CryptoProviderFactory cryptoProviderFactory) cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); } } + +#nullable enable + internal ValidationError? Verify( + SecurityKey key, + CryptoProviderFactory cryptoProviderFactory, +#pragma warning disable CA1801 // Review unused parameters + CallContext callContext) +#pragma warning restore CA1801 + { + if (key is null) + return ValidationError.NullParameter(nameof(key), new StackFrame()); + + if (cryptoProviderFactory is null) + return ValidationError.NullParameter(nameof(cryptoProviderFactory), new StackFrame()); + + if (SignedInfo is null) + return new XmlValidationError( + new MessageDetail(LogMessages.IDX30212), + ValidationFailureType.XmlValidationFailed, + typeof(XmlValidationException), + new StackFrame()); + + if (!cryptoProviderFactory.IsSupportedAlgorithm(SignedInfo.SignatureMethod, key)) + return new XmlValidationError( + new MessageDetail(LogMessages.IDX30207, SignedInfo.SignatureMethod, cryptoProviderFactory.GetType()), + ValidationFailureType.XmlValidationFailed, + typeof(XmlValidationException), + new StackFrame()); + + var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, SignedInfo.SignatureMethod); + if (signatureProvider is null) + return new XmlValidationError( + new MessageDetail(LogMessages.IDX30203, cryptoProviderFactory, key, SignedInfo.SignatureMethod), + ValidationFailureType.XmlValidationFailed, + typeof(XmlValidationException), + new StackFrame()); + + ValidationError? validationError = null; + + try + { + using (var memoryStream = new MemoryStream()) + { + SignedInfo.GetCanonicalBytes(memoryStream); + if (!signatureProvider.Verify(memoryStream.ToArray(), Convert.FromBase64String(SignatureValue))) + { + validationError = new XmlValidationError( + new MessageDetail(LogMessages.IDX30200, cryptoProviderFactory, key), + ValidationFailureType.XmlValidationFailed, + typeof(XmlValidationException), + new StackFrame()); + } + } + + if (validationError is null) + { + validationError = SignedInfo.Verify(cryptoProviderFactory, callContext); + validationError?.AddStackFrame(new StackFrame()); + } + } + finally + { + if (signatureProvider is not null) + cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); + } + + if (validationError is not null) + return validationError; + + return null; // no error + } +#nullable restore } } diff --git a/src/Microsoft.IdentityModel.Xml/SignedInfo.cs b/src/Microsoft.IdentityModel.Xml/SignedInfo.cs index 50c59ccf3a..7f18cdad4b 100644 --- a/src/Microsoft.IdentityModel.Xml/SignedInfo.cs +++ b/src/Microsoft.IdentityModel.Xml/SignedInfo.cs @@ -112,6 +112,39 @@ public void Verify(CryptoProviderFactory cryptoProviderFactory) reference.Verify(cryptoProviderFactory); } +#nullable enable + /// + /// Verifies the digest of all + /// + /// supplies any required cryptographic operators. + /// contextual information for diagnostics. + internal ValidationError? Verify( + CryptoProviderFactory cryptoProviderFactory, +#pragma warning disable CA1801 + CallContext callContext) +#pragma warning restore CA1801 + { + if (cryptoProviderFactory == null) + return ValidationError.NullParameter(nameof(cryptoProviderFactory), new System.Diagnostics.StackFrame()); + + ValidationError? validationError = null; + + for (int i = 0; i < References.Count; i++) + { + var reference = References[i]; + validationError = reference.Verify(cryptoProviderFactory, callContext); + + if (validationError is not null) + { + validationError.AddStackFrame(new System.Diagnostics.StackFrame()); + break; + } + } + + return validationError; + } +#nullable restore + /// /// Writes the Canonicalized bytes into a stream. /// From 9731a371674d69d6209d4ac2353e0efc3765d526 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 31 Oct 2024 16:08:50 +0000 Subject: [PATCH 03/14] Added XmlValidationFailure to ValidationFailureType --- .../InternalAPI.Unshipped.txt | 1 + .../Validation/Results/Details/ValidationError.cs | 4 ++++ .../Validation/ValidationFailureType.cs | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 5816053a79..e50d151726 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -32,3 +32,4 @@ static Microsoft.IdentityModel.Tokens.Utility.SerializeAsSingleCommaDelimitedStr 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.XmlValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs index 302476db22..8dec8466a0 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs @@ -118,6 +118,8 @@ internal Exception GetException(Type exceptionType, Exception innerException) exception = new SecurityTokenException(MessageDetail.Message); else if (exceptionType == typeof(SecurityTokenKeyWrapException)) exception = new SecurityTokenKeyWrapException(MessageDetail.Message); + else if (ExceptionType == typeof(SecurityTokenValidationException)) + exception = new SecurityTokenValidationException(MessageDetail.Message); else { // Exception type is unknown @@ -175,6 +177,8 @@ internal Exception GetException(Type exceptionType, Exception innerException) exception = new SecurityTokenException(MessageDetail.Message, actualException); else if (exceptionType == typeof(SecurityTokenKeyWrapException)) exception = new SecurityTokenKeyWrapException(MessageDetail.Message, actualException); + else if (exceptionType == typeof(SecurityTokenValidationException)) + exception = new SecurityTokenValidationException(MessageDetail.Message, actualException); else { // Exception type is unknown diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 7a5b3939c2..6cf24dc28c 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -110,5 +110,11 @@ private class TokenDecryptionFailure : ValidationFailureType { internal TokenDec /// public static readonly ValidationFailureType InvalidSecurityToken = new InvalidSecurityTokenFailure("InvalidSecurityToken"); private class InvalidSecurityTokenFailure : ValidationFailureType { internal InvalidSecurityTokenFailure(string name) : base(name) { } } + + /// + /// Defines a type that represents that an XML validation failed. + /// + public static readonly ValidationFailureType XmlValidationFailed = new XmlValidationFailure("XmlValidationFailed"); + private class XmlValidationFailure : ValidationFailureType { internal XmlValidationFailure(string name) : base(name) { } } } } From 08a2f19e78207fd242c10952644b524efa0c898f Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 31 Oct 2024 16:12:09 +0000 Subject: [PATCH 04/14] Added refactored ValidateSignature method to SamlSecurityTokenHandler. Updated ValidateTokenAsync to call ValidateSignature. --- .../InternalAPI.Unshipped.txt | 3 + ...lSecurityTokenHandler.ValidateSignature.cs | 167 ++++++++++++++++++ ...rityTokenHandler.ValidateToken.Internal.cs | 20 +++ ...yTokenHandler.ValidateToken.StackFrames.cs | 3 + .../Saml/SamlTokenUtilities.cs | 23 +++ 5 files changed, 216 insertions(+) create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index 4978a5a0a6..cfcf4a8218 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions 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.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> +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 @@ -19,6 +21,7 @@ static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames. static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.OneTimeUseValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenValidationParametersNull -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.ResolveTokenSigningKey(Microsoft.IdentityModel.Xml.KeyInfo tokenKeyInfo, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters) -> Microsoft.IdentityModel.Tokens.SecurityKey static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs new file mode 100644 index 0000000000..99e50582c3 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// 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; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml +{ + public partial class SamlSecurityTokenHandler : SecurityTokenHandler + { + internal static ValidationResult ValidateSignature( + SamlSecurityToken samlToken, + ValidationParameters validationParameters, +#pragma warning disable CA1801 // Review unused parameters + CallContext callContext) +#pragma warning restore CA1801 // Review unused parameters + { + if (samlToken is null) + { + return ValidationError.NullParameter( + nameof(samlToken), + new StackFrame(true)); + } + + if (validationParameters is null) + { + return ValidationError.NullParameter( + nameof(validationParameters), + new StackFrame(true)); + } + + // 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 implement the delegate + if (samlToken.Assertion.Signature is null) + return new XmlValidationError( + new MessageDetail( + TokenLogMessages.IDX10504, + samlToken.Assertion.CanonicalString), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenValidationException), + new StackFrame(true)); + + IList? keys = null; + 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); + } + + if (resolvedKey is null) + { + if (validationParameters.TryAllIssuerSigningKeys) + keys = validationParameters.IssuerSigningKeys; + } + else + { + keys = [resolvedKey]; + keyMatched = true; + } + + bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; + List errors = new(); + StringBuilder keysAttempted = new(); + + if (keys is not null) + { + for (int i = 0; i < keys.Count; i++) + { + SecurityKey key = keys[i]; + ValidationResult algorithmValidationResult = validationParameters.AlgorithmValidator( + samlToken.Assertion.Signature.SignedInfo.SignatureMethod, + key, + samlToken, + validationParameters, + callContext); + + if (!algorithmValidationResult.IsValid) + { + errors.Add(algorithmValidationResult.UnwrapError()); + } + else + { + var validationError = samlToken.Assertion.Signature.Verify( + key, + validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory, + callContext); + + if (validationError is null) + { + samlToken.SigningKey = key; + + return key; + } + else + { + errors.Add(validationError.AddStackFrame(new StackFrame())); + } + } + + keysAttempted.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(errors), + samlToken), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenInvalidSignatureException), + new StackFrame(true)); + + if (keysAttempted.Length > 0) + return new XmlValidationError( + new MessageDetail( + TokenLogMessages.IDX10512, + keysAttempted.ToString(), + GetErrorStrings(errors), + samlToken), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame(true)); + + return new XmlValidationError( + new MessageDetail(TokenLogMessages.IDX10500), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame(true)); + } + + private static string GetErrorStrings(List errors) + { + StringBuilder sb = new(); + for (int i = 0; i < errors.Count; i++) + { + sb.AppendLine(errors[i].ToString()); + } + + return sb.ToString(); + } + } +} +#nullable restore 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 f409264640..04a2d3c92e 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs @@ -48,6 +48,26 @@ internal async Task> ValidateTokenAsync( return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed); } + var issuerValidationResult = await validationParameters.IssuerValidatorAsync( + samlToken.Issuer, + samlToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (!issuerValidationResult.IsValid) + { + StackFrames.IssuerValidationFailed ??= new StackFrame(true); + return issuerValidationResult.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/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs index 8dc1d27cba..faa6aad0a6 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs @@ -22,6 +22,9 @@ 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/Saml/SamlTokenUtilities.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs index 525edc6e21..06842c5f3c 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs @@ -47,6 +47,29 @@ internal static SecurityKey ResolveTokenSigningKey(KeyInfo tokenKeyInfo, TokenVa return null; } + /// + /// Returns a to use when validating the signature of a token. + /// + /// The field of the token being validated + /// The to be used for validating the token. + /// Returns a to use for signature validation. + /// If key fails to resolve, then null is returned + internal static SecurityKey ResolveTokenSigningKey(KeyInfo tokenKeyInfo, ValidationParameters validationParameters) + { + if (tokenKeyInfo is null || validationParameters.IssuerSigningKeys is null) + return null; + + for (int i = 0; i < validationParameters.IssuerSigningKeys.Count; i++) + { + if (tokenKeyInfo.MatchesKey(validationParameters.IssuerSigningKeys[i])) + return validationParameters.IssuerSigningKeys[i]; + } + + return null; + } + + + /// /// Creates 's from . /// From 5fee17aea63608e93ece30cfa3b1c485b56fd0ea Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 31 Oct 2024 16:12:31 +0000 Subject: [PATCH 05/14] Added tests to compare signature validation between the legacy and new path --- ...Tests.ValidateTokenAsyncTests.Signature.cs | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs new file mode 100644 index 0000000000..d3714f73b4 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs @@ -0,0 +1,210 @@ +// 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; + +namespace Microsoft.IdentityModel.Tokens.Saml.Tests +{ +#nullable enable + public partial class SamlSecurityTokenHandlerTests + { + [Theory, MemberData(nameof(ValidateTokenAsync_Signature_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_SignatureComparison(ValidateTokenAsyncSignatureTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_SignatureComparison", 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; + var validationResultException = validationResult.UnwrapError().GetException(); + + if (theoryData.TestId == "Invalid_TokenSignedWithDifferentKey_KeyIdPresent_TryAllKeysFalse") + Console.WriteLine($"tokenValidationResultException: {tokenValidationResultException}"); + + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), 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 SamlSecurityToken CreateTokenForSignatureValidation(SigningCredentials? signingCredentials) + { + SamlSecurityTokenHandler samlTokenHandler = new SamlSecurityTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + Subject = Default.SamlClaimsIdentity, + SigningCredentials = signingCredentials, + Issuer = Default.Issuer, + }; + + SamlSecurityToken samlToken = (SamlSecurityToken)samlTokenHandler.CreateToken(securityTokenDescriptor); + + return samlTokenHandler.ReadSamlToken(samlToken.Assertion.CanonicalString); + } + } +} +#nullable restore From f9aeac7a796cf21c64274a08947bb75db83b7cbd Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Thu, 31 Oct 2024 16:16:53 +0000 Subject: [PATCH 06/14] Re-added API lost in merge to InternalAPI.Unshipped.txt --- .../InternalAPI.Unshipped.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index cfcf4a8218..057dee7277 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -9,6 +9,7 @@ Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions 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.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> +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 From ccae55158a9492f628981507a75dc672e3a35aa0 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Fri, 1 Nov 2024 11:28:50 +0000 Subject: [PATCH 07/14] Migrated refactored ValidateSignature from SamlSecurityTokenHandler to Saml2SecurityTokenHandler --- .../InternalAPI.Unshipped.txt | 2 + ...2SecurityTokenHandler.ValidateSignature.cs | 168 ++++++++++++++++++ ...yTokenHandler.ValidateToken.StackFrames.cs | 3 +- 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index 057dee7277..98bb8f1d05 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -30,6 +30,8 @@ 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.Saml.SamlSecurityTokenHandler.ValidateConditions(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult 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..ea1188668e --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics; +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, +#pragma warning disable CA1801 // Review unused parameters + CallContext callContext) +#pragma warning restore CA1801 // Review unused parameters + { + if (samlToken is null) + { + return ValidationError.NullParameter( + nameof(samlToken), + new StackFrame(true)); + } + + if (validationParameters is null) + { + return ValidationError.NullParameter( + nameof(validationParameters), + new StackFrame(true)); + } + + // 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 implement the delegate + if (samlToken.Assertion.Signature is null) + return new XmlValidationError( + new MessageDetail( + TokenLogMessages.IDX10504, + samlToken.Assertion.CanonicalString), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenValidationException), + new StackFrame(true)); + + IList? keys = null; + 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); + } + + if (resolvedKey is null) + { + if (validationParameters.TryAllIssuerSigningKeys) + keys = validationParameters.IssuerSigningKeys; + } + else + { + keys = [resolvedKey]; + keyMatched = true; + } + + bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; + List errors = new(); + StringBuilder keysAttempted = new(); + + if (keys is not null) + { + for (int i = 0; i < keys.Count; i++) + { + SecurityKey key = keys[i]; + ValidationResult algorithmValidationResult = validationParameters.AlgorithmValidator( + samlToken.Assertion.Signature.SignedInfo.SignatureMethod, + key, + samlToken, + validationParameters, + callContext); + + if (!algorithmValidationResult.IsValid) + { + errors.Add(algorithmValidationResult.UnwrapError()); + } + else + { + var validationError = samlToken.Assertion.Signature.Verify( + key, + validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory, + callContext); + + if (validationError is null) + { + samlToken.SigningKey = key; + + return key; + } + else + { + errors.Add(validationError.AddStackFrame(new StackFrame())); + } + } + + keysAttempted.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(errors), + samlToken), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenInvalidSignatureException), + new StackFrame(true)); + + if (keysAttempted.Length > 0) + return new XmlValidationError( + new MessageDetail( + TokenLogMessages.IDX10512, + keysAttempted.ToString(), + GetErrorStrings(errors), + samlToken), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame(true)); + + return new XmlValidationError( + new MessageDetail(TokenLogMessages.IDX10500), + ValidationFailureType.SignatureValidationFailed, + typeof(SecurityTokenSignatureKeyNotFoundException), + new StackFrame(true)); + } + + private static string GetErrorStrings(List errors) + { + StringBuilder sb = new(); + for (int i = 0; i < errors.Count; i++) + { + sb.AppendLine(errors[i].ToString()); + } + + return sb.ToString(); + } + } +} +#nullable restore 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; From bdb6133ed3cb281a89bfa0208e36adb46d5cb740 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Fri, 1 Nov 2024 11:29:33 +0000 Subject: [PATCH 08/14] Updated Saml2SecurityTokenHandler's ValidateTokenAsync to validate signatures --- .../Saml2SecurityTokenHandler.ValidateToken.Internal.cs | 7 +++++++ 1 file changed, 7 insertions(+) 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..6979857bdb 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -63,6 +63,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); } From 668eae311ae9890b69b15c43362c10a77845cccb Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Fri, 1 Nov 2024 11:30:05 +0000 Subject: [PATCH 09/14] Added tests --- ...Tests.ValidateTokenAsyncTests.Signature.cs | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs 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..3b86a84412 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs @@ -0,0 +1,211 @@ +// 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; + +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(); + + if (theoryData.TestId == "Invalid_TokenSignedWithDifferentKey_KeyIdPresent_TryAllKeysFalse") + Console.WriteLine($"tokenValidationResultException: {tokenValidationResultException}"); + + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, 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 From 0a7d1c4c2835001711b413b5ae78150ba8004c38 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Mon, 4 Nov 2024 21:25:02 +0000 Subject: [PATCH 10/14] Update src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs Co-authored-by: msbw2 --- .../Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs index ea1188668e..0cd559f430 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs @@ -38,7 +38,7 @@ internal static ValidationResult ValidateSignature( if (validationParameters.SignatureValidator is not null) return validationParameters.SignatureValidator(samlToken, validationParameters, null, callContext); - // If the user wants to accept unsigned tokens, they must implement the delegate + // If the user wants to accept unsigned tokens, they must set validationParameters.SignatureValidator if (samlToken.Assertion.Signature is null) return new XmlValidationError( new MessageDetail( From 6bc69cdf3a4d56561b5a55cd91c67402af75f7ee Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Tue, 5 Nov 2024 12:49:19 +0000 Subject: [PATCH 11/14] Addressed PR feedback in both SAML and SAML2 signature validations to keep the alignment --- ...lSecurityTokenHandler.ValidateSignature.cs | 28 ++++++++++++------- ...2SecurityTokenHandler.ValidateSignature.cs | 28 ++++++++++++------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs index 99e50582c3..196ed69069 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs @@ -78,8 +78,8 @@ internal static ValidationResult ValidateSignature( } bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; - List errors = new(); - StringBuilder keysAttempted = new(); + List? errors = null; + StringBuilder? keysAttempted = null; if (keys is not null) { @@ -95,7 +95,7 @@ internal static ValidationResult ValidateSignature( if (!algorithmValidationResult.IsValid) { - errors.Add(algorithmValidationResult.UnwrapError()); + (errors ??= new()).Add(algorithmValidationResult.UnwrapError()); } else { @@ -112,11 +112,11 @@ internal static ValidationResult ValidateSignature( } else { - errors.Add(validationError.AddStackFrame(new StackFrame())); + (errors ??= new()).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,7 +126,7 @@ internal static ValidationResult ValidateSignature( return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10514, - keysAttempted.ToString(), + keysAttempted?.ToString(), samlToken.Assertion.Signature.KeyInfo, GetErrorStrings(errors), samlToken), @@ -134,11 +134,11 @@ internal static ValidationResult ValidateSignature( typeof(SecurityTokenInvalidSignatureException), new StackFrame(true)); - if (keysAttempted.Length > 0) + if ((keysAttempted?.Length ?? 0) > 0) return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10512, - keysAttempted.ToString(), + keysAttempted!.ToString(), GetErrorStrings(errors), samlToken), ValidationFailureType.SignatureValidationFailed, @@ -152,12 +152,20 @@ internal static ValidationResult ValidateSignature( new StackFrame(true)); } - private static string GetErrorStrings(List errors) + private static string GetErrorStrings(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 (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].ToString()); + sb.AppendLine(errors[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 index 0cd559f430..5378301a4c 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs @@ -79,8 +79,8 @@ internal static ValidationResult ValidateSignature( } bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; - List errors = new(); - StringBuilder keysAttempted = new(); + List? errors = null; + StringBuilder? keysAttempted = null; if (keys is not null) { @@ -96,7 +96,7 @@ internal static ValidationResult ValidateSignature( if (!algorithmValidationResult.IsValid) { - errors.Add(algorithmValidationResult.UnwrapError()); + (errors ??= new()).Add(algorithmValidationResult.UnwrapError()); } else { @@ -113,11 +113,11 @@ internal static ValidationResult ValidateSignature( } else { - errors.Add(validationError.AddStackFrame(new StackFrame())); + (errors ??= new()).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); } @@ -127,7 +127,7 @@ internal static ValidationResult ValidateSignature( return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10514, - keysAttempted.ToString(), + keysAttempted?.ToString(), samlToken.Assertion.Signature.KeyInfo, GetErrorStrings(errors), samlToken), @@ -135,11 +135,11 @@ internal static ValidationResult ValidateSignature( typeof(SecurityTokenInvalidSignatureException), new StackFrame(true)); - if (keysAttempted.Length > 0) + if ((keysAttempted?.Length ?? 0) > 0) return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10512, - keysAttempted.ToString(), + keysAttempted!.ToString(), GetErrorStrings(errors), samlToken), ValidationFailureType.SignatureValidationFailed, @@ -153,12 +153,20 @@ internal static ValidationResult ValidateSignature( new StackFrame(true)); } - private static string GetErrorStrings(List errors) + private static string GetErrorStrings(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 (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].ToString()); + sb.AppendLine(errors[i].MessageDetail.Message); } return sb.ToString(); From 9c505b6ec57d9bf185c19f50dbe00e89e2ab2ea2 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Tue, 5 Nov 2024 15:24:06 +0000 Subject: [PATCH 12/14] Optimised signature validation in SAML and SAML2 for the expected most common scenario --- ...lSecurityTokenHandler.ValidateSignature.cs | 122 +++++++++++------- ...2SecurityTokenHandler.ValidateSignature.cs | 122 +++++++++++------- src/Microsoft.IdentityModel.Xml/Signature.cs | 15 +-- src/Microsoft.IdentityModel.Xml/SignedInfo.cs | 4 +- 4 files changed, 155 insertions(+), 108 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateSignature.cs index 196ed69069..e23f3c2267 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. @@ -45,7 +44,7 @@ internal static ValidationResult ValidateSignature( samlToken.Assertion.CanonicalString), ValidationFailureType.SignatureValidationFailed, typeof(SecurityTokenValidationException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); IList? keys = null; SecurityKey? resolvedKey = null; @@ -66,55 +65,38 @@ internal static ValidationResult ValidateSignature( resolvedKey = SamlTokenUtilities.ResolveTokenSigningKey(samlToken.Assertion.Signature.KeyInfo, validationParameters); } - if (resolvedKey is null) + bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; + List? errors = null; + ValidationError? error = null; + StringBuilder? keysAttempted = null; + + if (resolvedKey is not null) { - if (validationParameters.TryAllIssuerSigningKeys) - keys = validationParameters.IssuerSigningKeys; + keyMatched = true; + var result = ValidateSignatureUsingKey(resolvedKey, samlToken, validationParameters, callContext); + if (result.IsValid) + return result; + + error = result.UnwrapError(); } else { - keys = [resolvedKey]; - keyMatched = true; + if (validationParameters.TryAllIssuerSigningKeys) + keys = validationParameters.IssuerSigningKeys; } - bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; - List? errors = null; - StringBuilder? keysAttempted = null; - if (keys 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 < keys.Count; i++) { SecurityKey key = keys[i]; - ValidationResult algorithmValidationResult = validationParameters.AlgorithmValidator( - samlToken.Assertion.Signature.SignedInfo.SignatureMethod, - key, - samlToken, - validationParameters, - callContext); - - if (!algorithmValidationResult.IsValid) - { - (errors ??= new()).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; - - return key; - } - else - { - (errors ??= new()).Add(validationError.AddStackFrame(new StackFrame())); - } - } + (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) @@ -128,34 +110,76 @@ internal static ValidationResult ValidateSignature( TokenLogMessages.IDX10514, keysAttempted?.ToString(), samlToken.Assertion.Signature.KeyInfo, - GetErrorStrings(errors), + GetErrorStrings(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) > 0) + if (keysAttemptedString is not null) return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10512, - keysAttempted!.ToString(), - GetErrorStrings(errors), + keysAttemptedString, + GetErrorStrings(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 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; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs index 5378301a4c..20df361659 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateSignature.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.Diagnostics; using System.Text; using Microsoft.IdentityModel.Tokens.Saml; using Microsoft.IdentityModel.Xml; @@ -24,14 +23,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. @@ -46,7 +45,7 @@ internal static ValidationResult ValidateSignature( samlToken.Assertion.CanonicalString), ValidationFailureType.SignatureValidationFailed, typeof(SecurityTokenValidationException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); IList? keys = null; SecurityKey? resolvedKey = null; @@ -67,55 +66,38 @@ internal static ValidationResult ValidateSignature( resolvedKey = SamlTokenUtilities.ResolveTokenSigningKey(samlToken.Assertion.Signature.KeyInfo, validationParameters); } - if (resolvedKey is null) + bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; + List? errors = null; + ValidationError? error = null; + StringBuilder? keysAttempted = null; + + if (resolvedKey is not null) { - if (validationParameters.TryAllIssuerSigningKeys) - keys = validationParameters.IssuerSigningKeys; + keyMatched = true; + var result = ValidateSignatureUsingKey(resolvedKey, samlToken, validationParameters, callContext); + if (result.IsValid) + return result; + + error = result.UnwrapError(); } else { - keys = [resolvedKey]; - keyMatched = true; + if (validationParameters.TryAllIssuerSigningKeys) + keys = validationParameters.IssuerSigningKeys; } - bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null; - List? errors = null; - StringBuilder? keysAttempted = null; - if (keys 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 < keys.Count; i++) { SecurityKey key = keys[i]; - ValidationResult algorithmValidationResult = validationParameters.AlgorithmValidator( - samlToken.Assertion.Signature.SignedInfo.SignatureMethod, - key, - samlToken, - validationParameters, - callContext); - - if (!algorithmValidationResult.IsValid) - { - (errors ??= new()).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; - - return key; - } - else - { - (errors ??= new()).Add(validationError.AddStackFrame(new StackFrame())); - } - } + (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) @@ -129,34 +111,76 @@ internal static ValidationResult ValidateSignature( TokenLogMessages.IDX10514, keysAttempted?.ToString(), samlToken.Assertion.Signature.KeyInfo, - GetErrorStrings(errors), + GetErrorStrings(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) > 0) + if (keysAttemptedString is not null) return new XmlValidationError( new MessageDetail( TokenLogMessages.IDX10512, - keysAttempted!.ToString(), - GetErrorStrings(errors), + keysAttemptedString, + GetErrorStrings(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, 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; 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; } } From b00716c506fac34a42a4b1187142b228178a4e6e Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Tue, 5 Nov 2024 15:26:13 +0000 Subject: [PATCH 13/14] Removed debug information --- ...tyTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs index 3b86a84412..dbe28f7582 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Signature.cs @@ -1,7 +1,6 @@ // 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; @@ -55,10 +54,7 @@ await saml2TokenHandler.ValidateTokenAsync( var tokenValidationResultException = tokenValidationResult.Exception; var validationResultException = validationResult.UnwrapError().GetException(); - if (theoryData.TestId == "Invalid_TokenSignedWithDifferentKey_KeyIdPresent_TryAllKeysFalse") - Console.WriteLine($"tokenValidationResultException: {tokenValidationResultException}"); - - theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + theoryData.ExpectedException.ProcessException(tokenValidationResultException, context); theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResultException, context); } From 7129fe69080ab46531dc360a255f710388a96a52 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 6 Nov 2024 16:57:27 +0000 Subject: [PATCH 14/14] Added tests for SAML and SAML2 algorithm validation using the new validation model --- ...Tests.ValidateTokenAsyncTests.Algorithm.cs | 201 ++++++++++++++++++ ...Tests.ValidateTokenAsyncTests.Algorithm.cs | 200 +++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs new file mode 100644 index 0000000000..13581b3d01 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +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_Algorithm_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_AlgorithmComparison(ValidateTokenAsyncAlgorithmTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_AlgorithmComparison", 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(); + + if (theoryData.TestId == "Invalid_TokenSignedWithDifferentKey_KeyIdPresent_TryAllKeysFalse") + Console.WriteLine($"tokenValidationResultException: {tokenValidationResultException}"); + + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context); + } + + TestUtilities.AssertFailIfErrors(context); + } + } + + public static TheoryData ValidateTokenAsync_Algorithm_TestCases + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_AlgorithmIsValid") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.RsaSha256Signature]), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.RsaSha256Signature]), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_ValidAlgorithmsIsNull") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: null), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: null), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_ValidAlgorithmsIsEmptyList") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, validAlgorithms: []), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, validAlgorithms: []), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Invalid_TokenIsSignedWithAnInvalidAlgorithm_TryAllKeysFalse") + { + // Token is signed with HmacSha256 but only sha256 is considered valid for this test's purposes + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: false), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: false), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Invalid_TokenIsSignedWithAnInvalidAlgorithm_TryAllKeysTrue") + { + // Token is signed with HmacSha256 but only sha256 is considered valid for this test's purposes + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: true), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: true), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters( + SecurityKey? signingKey = null, List? validAlgorithms = null, bool tryAllKeys = false) + { + ValidationParameters validationParameters = new ValidationParameters(); + + if (signingKey is not null) + validationParameters.IssuerSigningKeys.Add(signingKey); + + validationParameters.ValidAlgorithms = validAlgorithms; + + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + validationParameters.TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + validationParameters.TryAllIssuerSigningKeys = tryAllKeys; + + return validationParameters; + } + + static TokenValidationParameters CreateTokenValidationParameters( + SecurityKey? signingKey = null, List? validAlgorithms = null, bool tryAllKeys = false) + { + return new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = false, + RequireSignedTokens = true, + RequireAudience = false, + IssuerSigningKey = signingKey, + ValidAlgorithms = validAlgorithms, + TryAllIssuerSigningKeys = tryAllKeys, + }; + } + } + } + + public class ValidateTokenAsyncAlgorithmTheoryData : TheoryDataBase + { + public ValidateTokenAsyncAlgorithmTheoryData(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; } + } + + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs new file mode 100644 index 0000000000..5d1a77c4c6 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Algorithm.cs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Saml.Tests +{ +#nullable enable + public partial class SamlSecurityTokenHandlerTests + { + [Theory, MemberData(nameof(ValidateTokenAsync_Algorithm_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_AlgorithmComparison(ValidateTokenAsyncAlgorithmTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_AlgorithmComparison", 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; + var validationResultException = validationResult.UnwrapError().GetException(); + + if (theoryData.TestId == "Invalid_TokenSignedWithDifferentKey_KeyIdPresent_TryAllKeysFalse") + Console.WriteLine($"tokenValidationResultException: {tokenValidationResultException}"); + + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context); + } + + TestUtilities.AssertFailIfErrors(context); + } + } + + public static TheoryData ValidateTokenAsync_Algorithm_TestCases + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_AlgorithmIsValid") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.RsaSha256Signature]), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.RsaSha256Signature]), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_ValidAlgorithmsIsNull") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: null), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, + validAlgorithms: null), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Valid_ValidAlgorithmsIsEmptyList") + { + SigningCredentials = KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, validAlgorithms: []), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultX509SigningCreds_2048_RsaSha2_Sha2.Key, validAlgorithms: []), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Invalid_TokenIsSignedWithAnInvalidAlgorithm_TryAllKeysFalse") + { + // Token is signed with HmacSha256 but only sha256 is considered valid for this test's purposes + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: false), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: false), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10500:"), + }); + + theoryData.Add(new ValidateTokenAsyncAlgorithmTheoryData("Invalid_TokenIsSignedWithAnInvalidAlgorithm_TryAllKeysTrue") + { + // Token is signed with HmacSha256 but only sha256 is considered valid for this test's purposes + SigningCredentials = KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2, + TokenValidationParameters = CreateTokenValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: true), + ValidationParameters = CreateValidationParameters( + KeyingMaterial.DefaultSymmetricSigningCreds_256_Sha2.Key, + validAlgorithms: [SecurityAlgorithms.Sha256], + tryAllKeys: true), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenSignatureKeyNotFoundException("IDX10512:"), + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters( + SecurityKey? signingKey = null, List? validAlgorithms = null, bool tryAllKeys = false) + { + ValidationParameters validationParameters = new ValidationParameters(); + + if (signingKey is not null) + validationParameters.IssuerSigningKeys.Add(signingKey); + + validationParameters.ValidAlgorithms = validAlgorithms; + + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + validationParameters.TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation; + validationParameters.TryAllIssuerSigningKeys = tryAllKeys; + + return validationParameters; + } + + static TokenValidationParameters CreateTokenValidationParameters( + SecurityKey? signingKey = null, List? validAlgorithms = null, bool tryAllKeys = false) + { + return new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = false, + RequireSignedTokens = true, + RequireAudience = false, + IssuerSigningKey = signingKey, + ValidAlgorithms = validAlgorithms, + TryAllIssuerSigningKeys = tryAllKeys, + }; + } + } + } + + public class ValidateTokenAsyncAlgorithmTheoryData : TheoryDataBase + { + public ValidateTokenAsyncAlgorithmTheoryData(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; } + } + + } +} +#nullable restore