From 69b15a7bb34103680a9bebb192366e34c71e993b Mon Sep 17 00:00:00 2001 From: Franco Fung <38921563+FuPingFranco@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:23:59 -0700 Subject: [PATCH] Implement and Test Audience and Lifetime validations in SamlSecurityTokenHandler with New Validation Model (#2925) * Implement and test Audience and Lifetime validations in SamlSecurityTokenHandler using new validation model * Removed unesserasary method declaration in InternalAPI.Unshipped.txt file * Addressing PR feedback. Cached condition validation filure stackFrame * Clean-up --------- Co-authored-by: Franco Fung Co-authored-by: Ignacio Inglese --- .../InternalAPI.Unshipped.txt | 20 ++ ...rityTokenHandler.ValidateToken.Internal.cs | 121 ++++++++ ...yTokenHandler.ValidateToken.StackFrames.cs | 28 ++ .../Saml/SamlSecurityTokenHandler.cs | 2 +- ...rityTokenHandler.ValidateToken.Internal.cs | 13 +- ...yTokenHandler.ValidateToken.StackFrames.cs | 1 + ...rTests.ValidateTokenAsyncTests.Audience.cs | 4 +- ...rTests.ValidateTokenAsyncTests.Lifetime.cs | 4 +- ...rTests.ValidateTokenAsyncTests.Audience.cs | 239 ++++++++++++++++ ...rTests.ValidateTokenAsyncTests.Lifetime.cs | 260 ++++++++++++++++++ .../SamlSecurityTokenHandlerTests.cs | 2 +- 11 files changed, 685 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs create mode 100644 src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index c3a63e3044..5396e123a7 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -1,9 +1,29 @@ +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedAudience.get -> string +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedAudience.set -> void +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedConditions() -> void +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedConditions(string ValidatedAudience, Microsoft.IdentityModel.Tokens.ValidatedLifetime? ValidatedLifetime) -> void +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedLifetime.get -> Microsoft.IdentityModel.Tokens.ValidatedLifetime? +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedLifetime.set -> void +Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +Microsoft.IdentityModel.Tokens.Saml2.SamlSecurityTokenHandler.ValidateTokenAsync(SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AudienceValidationFailed -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.LifetimeValidationFailed -> System.Diagnostics.StackFrame +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.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 static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AudienceValidationFailed -> 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.TokenNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.TokenValidationParametersNull -> System.Diagnostics.StackFrame +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/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs new file mode 100644 index 0000000000..f409264640 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml +{ + /// + /// A designed for creating and validating Saml Tokens. See: http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf + /// + public partial class SamlSecurityTokenHandler : SecurityTokenHandler + { +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + internal async Task> ValidateTokenAsync( +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + SamlSecurityToken samlToken, + ValidationParameters validationParameters, + CallContext callContext, +#pragma warning disable CA1801 // Review unused parameters + CancellationToken cancellationToken) +#pragma warning restore CA1801 // Review unused parameters + { + if (samlToken is null) + { + StackFrames.TokenNull ??= new StackFrame(true); + return ValidationError.NullParameter( + nameof(samlToken), + StackFrames.TokenNull); + } + + if (validationParameters is null) + { + StackFrames.TokenValidationParametersNull ??= new StackFrame(true); + return ValidationError.NullParameter( + nameof(validationParameters), + StackFrames.TokenValidationParametersNull); + } + + var conditionsResult = ValidateConditions(samlToken, validationParameters, callContext); + + if (!conditionsResult.IsValid) + { + StackFrames.AssertionConditionsValidationFailed ??= new StackFrame(true); + return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed); + } + + return new ValidatedToken(samlToken, this, validationParameters); + } + + // ValidatedConditions is basically a named tuple but using a record struct better expresses the intent. + internal record struct ValidatedConditions(string? ValidatedAudience, ValidatedLifetime? ValidatedLifetime); + + internal virtual ValidationResult ValidateConditions( + SamlSecurityToken samlToken, + ValidationParameters validationParameters, + CallContext callContext) + { + if (samlToken.Assertion is null) + { + StackFrames.AssertionNull ??= new StackFrame(true); + return ValidationError.NullParameter( + nameof(samlToken.Assertion), + StackFrames.AssertionNull); + } + + if (samlToken.Assertion.Conditions is null) + { + StackFrames.AssertionConditionsNull ??= new StackFrame(true); + return ValidationError.NullParameter( + nameof(samlToken.Assertion.Conditions), + StackFrames.AssertionConditionsNull); + } + + var lifetimeValidationResult = validationParameters.LifetimeValidator( + samlToken.Assertion.Conditions.NotBefore, + samlToken.Assertion.Conditions.NotOnOrAfter, + samlToken, + validationParameters, + callContext); + + if (!lifetimeValidationResult.IsValid) + { + StackFrames.LifetimeValidationFailed ??= new StackFrame(true); + return lifetimeValidationResult.UnwrapError().AddStackFrame(StackFrames.LifetimeValidationFailed); + } + + string? validatedAudience = null; + foreach (var condition in samlToken.Assertion.Conditions.Conditions) + { + + if (condition is SamlAudienceRestrictionCondition audienceRestriction) + { + + // AudienceRestriction.Audiences is an ICollection so we need make a conversion to List before calling our audience validator + var audiencesAsList = audienceRestriction.Audiences.Select(static x => x.OriginalString).ToList(); + + var audienceValidationResult = validationParameters.AudienceValidator( + audiencesAsList, + samlToken, + validationParameters, + callContext); + + if (!audienceValidationResult.IsValid) + return audienceValidationResult.UnwrapError(); + + validatedAudience = audienceValidationResult.UnwrapResult(); + } + + if (validatedAudience != null) + break; + } + + return new ValidatedConditions(validatedAudience, lifetimeValidationResult.UnwrapResult()); + } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs new file mode 100644 index 0000000000..8dc1d27cba --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml +{ + public partial class SamlSecurityTokenHandler : SecurityTokenHandler + { + // Cached stack frames to build exceptions from validation errors + internal static class StackFrames + { + // Stack frames from ValidateTokenAsync using SecurityToken + internal static StackFrame? TokenNull; + internal static StackFrame? TokenValidationParametersNull; + + // Stack frames from ValidateConditions + internal static StackFrame? AudienceValidationFailed; + internal static StackFrame? AssertionNull; + internal static StackFrame? AssertionConditionsNull; + internal static StackFrame? AssertionConditionsValidationFailed; + internal static StackFrame? LifetimeValidationFailed; + internal static StackFrame? OneTimeUseValidationFailed; + } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index d82ff3f75e..4f7bfe9e54 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -22,7 +22,7 @@ namespace Microsoft.IdentityModel.Tokens.Saml /// which supports validating tokens passed as strings using . /// /// - public class SamlSecurityTokenHandler : SecurityTokenHandler + public partial class SamlSecurityTokenHandler : SecurityTokenHandler { internal const string Actor = "Actor"; private const string _className = "Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler"; 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 239c9b66ae..2bb12a27e5 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -44,7 +44,8 @@ internal async Task> ValidateTokenAsync( if (!conditionsResult.IsValid) { - return conditionsResult.UnwrapError().AddStackFrame(new StackFrame(true)); + StackFrames.AssertionConditionsValidationFailed ??= new StackFrame(true); + return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed); } return new ValidatedToken(samlToken, this, validationParameters); @@ -53,7 +54,10 @@ internal async Task> ValidateTokenAsync( // ValidatedConditions is basically a named tuple but using a record struct better expresses the intent. internal record struct ValidatedConditions(string? ValidatedAudience, ValidatedLifetime? ValidatedLifetime); - internal virtual ValidationResult ValidateConditions(Saml2SecurityToken samlToken, ValidationParameters validationParameters, CallContext callContext) + internal virtual ValidationResult ValidateConditions( + Saml2SecurityToken samlToken, + ValidationParameters validationParameters, + CallContext callContext) { if (samlToken.Assertion is null) { @@ -129,7 +133,10 @@ internal virtual ValidationResult ValidateConditions(Saml2S validationParameters, callContext); if (!audienceValidationResult.IsValid) - return audienceValidationResult.UnwrapError(); + { + StackFrames.AudienceValidationFailed ??= new StackFrame(true); + return audienceValidationResult.UnwrapError().AddStackFrame(StackFrames.AudienceValidationFailed); + } // Audience is valid, save it for later. validatedAudience = audienceValidationResult.UnwrapResult(); 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 61466f8406..65c3bed36a 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs @@ -19,6 +19,7 @@ internal static class StackFrames internal static StackFrame? AudienceValidationFailed; internal static StackFrame? AssertionNull; internal static StackFrame? AssertionConditionsNull; + internal static StackFrame? AssertionConditionsValidationFailed; internal static StackFrame? LifetimeValidationFailed; internal static StackFrame? OneTimeUseValidationFailed; } diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs index fd6ec72fba..2c04472039 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs @@ -22,7 +22,7 @@ public async Task ValidateTokenAsync_AudienceComparison(ValidateTokenAsyncAudien Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); - var saml2Token = CreateToken(theoryData.TokenAudience!, theoryData.Saml2Condition!); + var saml2Token = CreateTokenForAudienceValidation(theoryData.TokenAudience!, theoryData.Saml2Condition!); var tokenValidationParameters = CreateTokenValidationParameters( theoryData.TVPAudiences, @@ -200,7 +200,7 @@ public ValidateTokenAsyncAudienceTheoryData(string testId) : base(testId) { } public List? TVPAudiences { get; internal set; } } - private static Saml2SecurityToken CreateToken(string audience, Saml2Conditions saml2Conditions) + private static Saml2SecurityToken CreateTokenForAudienceValidation(string audience, Saml2Conditions saml2Conditions) { Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs index 5aa44aaebe..a2478ffdb9 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs @@ -18,7 +18,7 @@ public async Task ValidateTokenAsync_LifetimeComparison(ValidateTokenAsyncLifeti { var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_LifetimeComparison", theoryData); - var saml2Token = CreateToken( + var saml2Token = CreateTokenForLifetimeValidation( theoryData.IssuedAt, theoryData.NotBefore, theoryData.Expires); @@ -209,7 +209,7 @@ public ValidateTokenAsyncLifetimeTheoryData(string testId) : base(testId) { } public bool NullTokenValidationParameters { get; internal set; } = false; } - private static Saml2SecurityToken CreateToken(DateTime? issuedAt, DateTime? notBefore, DateTime? expires) + private static Saml2SecurityToken CreateTokenForLifetimeValidation(DateTime? issuedAt, DateTime? notBefore, DateTime? expires) { Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs new file mode 100644 index 0000000000..8249bf8e95 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs @@ -0,0 +1,239 @@ +// 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_Audience_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_AudienceComparison(ValidateTokenAsyncAudienceTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_AudienceComparison", theoryData); + + SamlSecurityTokenHandler samlTokenHandler = new SamlSecurityTokenHandler(); + + var samlToken = CreateTokenForAudienceValidation(theoryData.TokenAudience!); + + var tokenValidationParameters = CreateTokenValidationParameters( + theoryData.TVPAudiences, + samlToken, + theoryData.NullTokenValidationParameters, + theoryData.IgnoreTrailingSlashWhenValidatingAudience); + + // Validate the token using TokenValidationParameters + TokenValidationResult tokenValidationResult = + await samlTokenHandler.ValidateTokenAsync(samlToken.Assertion.CanonicalString, tokenValidationParameters); + + // Validate the token using ValidationParameters. + ValidationResult validationResult = + await samlTokenHandler.ValidateTokenAsync( + samlToken, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + // Ensure the validity of the results match the expected result. + if (tokenValidationResult.IsValid != validationResult.IsValid) + { + context.AddDiff($"tokenValidationResult.IsValid != validationResult.IsSuccess"); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context); + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + } + else + { + if (tokenValidationResult.IsValid) + { + // Verify that the validated tokens from both paths match. + ValidatedToken validatedToken = validationResult.UnwrapResult(); + IdentityComparer.AreEqual(validatedToken.SecurityToken, tokenValidationResult.SecurityToken, context); + } + else + { + // Verify the exception provided by both paths match. + var tokenValidationResultException = tokenValidationResult.Exception; + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + var validationResultException = validationResult.UnwrapError().GetException(); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context); + } + + TestUtilities.AssertFailIfErrors(context); + } + } + + public static TheoryData ValidateTokenAsync_Audience_TestCases + { + get + { + + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Valid_AudiencesMatch") + { + TokenAudience = Default.Audience, + TVPAudiences = [Default.Audience], + ValidationParameters = CreateValidationParameters([Default.Audience]) + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Valid_AudienceWithinValidAudiences") + { + TokenAudience = Default.Audience, + TVPAudiences = ["ExtraAudience", Default.Audience, "AnotherAudience"], + ValidationParameters = CreateValidationParameters(["ExtraAudience", Default.Audience, "AnotherAudience"]), + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Valid_AudienceWithSlash_IgnoreTrailingSlashTrue") + { + // Audience has a trailing slash, but IgnoreTrailingSlashWhenValidatingAudience is true. + TokenAudience = Default.Audience + "/", + TVPAudiences = [Default.Audience], + IgnoreTrailingSlashWhenValidatingAudience = true, + ValidationParameters = CreateValidationParameters([Default.Audience], true), + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Valid_ValidAudiencesWithSlash_IgnoreTrailingSlashTrue") + { + // ValidAudiences has a trailing slash, but IgnoreTrailingSlashWhenValidatingAudience is true. + TokenAudience = Default.Audience, + IgnoreTrailingSlashWhenValidatingAudience = true, + TVPAudiences = [Default.Audience + "/"], + ValidationParameters = CreateValidationParameters([Default.Audience + "/"], true), + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Invalid_AudiencesDoNotMatch") + { + //This test will cover scenarios where audience is whitespace, null or empty as SamlAudienceRestrictionCondition.Audiences are returned as Uri objects instead of Strings. + ValidationParameters = CreateValidationParameters([Default.Audience]), + TokenAudience = "http://NotOurDefault.Audience.com", + TVPAudiences = [Default.Audience], + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10215:"), + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Invalid_AudienceWithSlash_IgnoreTrailingSlashFalse") + { + // Audience has a trailing slash and IgnoreTrailingSlashWhenValidatingAudience is false. + TokenAudience = Default.Audience + "/", + TVPAudiences = [Default.Audience], + ValidationParameters = CreateValidationParameters([Default.Audience], false), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10215:"), + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Invalid_ValidAudiencesWithSlash_IgnoreTrailingSlashFalse") + { + // ValidAudiences has a trailing slash and IgnoreTrailingSlashWhenValidatingAudience is false. + TokenAudience = Default.Audience, + TVPAudiences = [Default.Audience + "/"], + ValidationParameters = CreateValidationParameters([Default.Audience + "/"], false), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10215:"), + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Invalid_TokenValidationParametersAndValidationParametersAreNull") + { + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + ExpectedIsValid = false, + TokenAudience = Default.Audience, + TVPAudiences = [Default.Audience], + ValidationParameters = null, + NullTokenValidationParameters = true + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters( + List? audiences, + bool ignoreTrailingSlashWhenValidatingAudience = false) + { + ValidationParameters validationParameters = new ValidationParameters(); + audiences?.ForEach(audience => validationParameters.ValidAudiences.Add(audience)); + validationParameters.IgnoreTrailingSlashWhenValidatingAudience = ignoreTrailingSlashWhenValidatingAudience; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.TokenReplayValidator = SkipValidationDelegates.SkipTokenReplayValidation; + + return validationParameters; + } + } + } + + public class ValidateTokenAsyncAudienceTheoryData : TheoryDataBase + { + public ValidateTokenAsyncAudienceTheoryData(string testId) : base(testId) { } + + internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = ExpectedException.NoExceptionExpected; + + internal bool ExpectedIsValid { get; set; } = true; + + public bool IgnoreTrailingSlashWhenValidatingAudience { get; internal set; } = false; + + public bool NullTokenValidationParameters { get; internal set; } = false; + + internal ValidationParameters? ValidationParameters { get; set; } + + public string? TokenAudience { get; internal set; } + + public List? TVPAudiences { get; internal set; } + } + + private static SamlSecurityToken CreateTokenForAudienceValidation(string audience) + { + SamlSecurityTokenHandler samlTokenHandler = new SamlSecurityTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + Expires = DateTime.UtcNow + TimeSpan.FromDays(1), + Audience = audience, + SigningCredentials = Default.AsymmetricSigningCredentials, + Issuer = Default.Issuer, + Subject = Default.SamlClaimsIdentity + }; + + SamlSecurityToken samlToken = (SamlSecurityToken)samlTokenHandler.CreateToken(securityTokenDescriptor); + + return samlToken; + } + + private static TokenValidationParameters? CreateTokenValidationParameters( + List? audiences, + SamlSecurityToken samlSecurityToken, + bool nullTokenValidationParameters, + bool ignoreTrailingSlashWhenValidatingAudience = false) + { + if (nullTokenValidationParameters) + { + return null; + } + + return new TokenValidationParameters + { + ValidateAudience = true, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = false, + RequireSignedTokens = false, + ValidAudiences = audiences, + IgnoreTrailingSlashWhenValidatingAudience = ignoreTrailingSlashWhenValidatingAudience, + SignatureValidator = delegate (string token, TokenValidationParameters validationParameters) + { + return samlSecurityToken; + }, + RequireAudience = true + }; + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs new file mode 100644 index 0000000000..d4ae7cd788 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.ValidateTokenAsyncTests.Lifetime.cs @@ -0,0 +1,260 @@ +// 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_LifetimeTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_LifetimeComparison(ValidateTokenAsyncLifetimeTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_LifetimeComparison", theoryData); + + var samlToken = CreateTokenForLifetimeValidation( + theoryData.IssuedAt, + theoryData.NotBefore, + theoryData.Expires); + + var tokenValidationParameters = CreateTokenValidationParameters( + samlToken, + theoryData.NullTokenValidationParameters, + theoryData.ClockSkew); + + SamlSecurityTokenHandler samlTokenHandler = new SamlSecurityTokenHandler(); + + // Validate token using TokenValidationParameters + TokenValidationResult tokenValidationResult = + await samlTokenHandler.ValidateTokenAsync(samlToken.Assertion.CanonicalString, tokenValidationParameters); + + // Validate token using ValidationParameters. + ValidationResult validationResult = + await samlTokenHandler.ValidateTokenAsync( + samlToken, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + // Ensure 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 validated tokens from both paths match. + ValidatedToken validatedToken = validationResult.UnwrapResult(); + IdentityComparer.AreEqual(validatedToken.SecurityToken, tokenValidationResult.SecurityToken, context); + } + else + { + // Verify the exception provided by both paths match. + var tokenValidationResultException = tokenValidationResult.Exception; + theoryData.ExpectedException.ProcessException(tokenValidationResult.Exception, context); + var validationResultException = validationResult.UnwrapError().GetException(); + theoryData.ExpectedExceptionValidationParameters!.ProcessException(validationResult.UnwrapError().GetException(), context); + } + + TestUtilities.AssertFailIfErrors(context); + } + } + + public static TheoryData ValidateTokenAsync_LifetimeTestCases + { + get + { + var theoryData = new TheoryData(); + + DateTime now = DateTime.UtcNow; + DateTime nowPlus1Hour = now.AddHours(1); + DateTime nowMinus1Hour = now.AddHours(-1); + DateTime nowPlus3Minutes = now.AddMinutes(3); + DateTime nowMinus3Minutes = now.AddMinutes(-3); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Valid_LifetimeIsValid") + { + IssuedAt = now, + NotBefore = nowMinus1Hour, + Expires = nowPlus1Hour, + ValidationParameters = CreateValidationParameters() + }); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Valid_ExpiredThreeMinutesAgoButSkewIsFiveMinutes") + { + // Default clock skew is 5 minutes. + IssuedAt = nowMinus1Hour, + NotBefore = nowMinus1Hour, + Expires = nowMinus3Minutes, + ValidationParameters = CreateValidationParameters() + }); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Valid_ValidInThreeMinutesButSkewIsFiveMinutes") + { + // Default clock skew is 5 minutes. + IssuedAt = nowMinus1Hour, + NotBefore = nowPlus3Minutes, + Expires = nowPlus1Hour, + ValidationParameters = CreateValidationParameters() + }); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Invalid_TokenHasNoExpiration") + { + IssuedAt = now, + NotBefore = nowMinus1Hour, + Expires = null, + ValidationParameters = CreateValidationParameters(), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenNoExpirationException("IDX10225:") + }); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Invalid_NotBeforeIsAfterExpires") + { + IssuedAt = nowMinus1Hour, + NotBefore = nowPlus1Hour, + Expires = now, + ValidationParameters = CreateValidationParameters(), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidLifetimeException("IDX10224:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidLifetimeException("IDX10224:") + }); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Invalid_ExpiredThreeMinutesAgoButSkewIsTwoMinutes") + { + // We override the clock skew to 2 minutes. + IssuedAt = nowMinus1Hour, + NotBefore = nowMinus1Hour, + Expires = nowMinus3Minutes, + ClockSkew = TimeSpan.FromMinutes(2), + ValidationParameters = CreateValidationParameters(TimeSpan.FromMinutes(2)), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenExpiredException("IDX10223:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenExpiredException("IDX10223:") + }); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Invalid_ValidInThreeMinutesButSkewIsTwoMinutes") + { + // We override the clock skew to 2 minutes. + IssuedAt = nowMinus1Hour, + NotBefore = nowPlus3Minutes, + Expires = nowPlus1Hour, + ClockSkew = TimeSpan.FromMinutes(2), + ValidationParameters = CreateValidationParameters(TimeSpan.FromMinutes(2)), + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenNotYetValidException("IDX10222:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenNotYetValidException("IDX10222:") + }); + + theoryData.Add(new ValidateTokenAsyncLifetimeTheoryData("Invalid_TokenValidationParametersAndValidationParametersAreNull") + { + IssuedAt = nowMinus1Hour, + NotBefore = nowPlus3Minutes, + Expires = nowPlus1Hour, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenArgumentNullException("IDX10000:"), + ExpectedIsValid = false, + ValidationParameters = null, + NullTokenValidationParameters = true + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters(TimeSpan? clockSkew = null) + { + ValidationParameters validationParameters = new ValidationParameters(); + + if (clockSkew is not null) + validationParameters.ClockSkew = clockSkew.Value; + + // Skip all validations except lifetime + validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation; + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation; + validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation; + validationParameters.SignatureValidator = SkipValidationDelegates.SkipSignatureValidation; + + return validationParameters; + } + } + } + + public class ValidateTokenAsyncLifetimeTheoryData : TheoryDataBase + { + public ValidateTokenAsyncLifetimeTheoryData(string testId) : base(testId) { } + + internal ValidationParameters? ValidationParameters { get; set; } + + internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = ExpectedException.NoExceptionExpected; + + internal bool ExpectedIsValid { get; set; } = true; + + public TimeSpan? ClockSkew { get; internal set; } = null; + + public DateTime? IssuedAt { get; set; } + + public DateTime? NotBefore { get; set; } + + public DateTime? Expires { get; set; } + + public bool NullTokenValidationParameters { get; internal set; } = false; + } + + private static SamlSecurityToken CreateTokenForLifetimeValidation(DateTime? issuedAt, DateTime? notBefore, DateTime? expires) + { + SamlSecurityTokenHandler samlTokenHandler = new SamlSecurityTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + IssuedAt = issuedAt, + NotBefore = notBefore, + Expires = expires, + SigningCredentials = Default.AsymmetricSigningCredentials, + Audience = Default.Audience, + Issuer = Default.Issuer, + Subject = Default.SamlClaimsIdentity + }; + + return (SamlSecurityToken)samlTokenHandler.CreateToken(securityTokenDescriptor); + } + + private static TokenValidationParameters? CreateTokenValidationParameters( + SamlSecurityToken samlSecurityToken, + bool nullTokenValidationParameters, + TimeSpan? clockSkew = null) + { + if (nullTokenValidationParameters) + { + return null; + } + + var tokenValidationParameters = new TokenValidationParameters() + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = true, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = false, + RequireSignedTokens = false, + SignatureValidator = delegate (string token, TokenValidationParameters validationParameters) + { + return samlSecurityToken; + } + }; + + if (clockSkew is not null) + tokenValidationParameters.ClockSkew = clockSkew.Value; + + return tokenValidationParameters; + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs index 3ee02af2e3..406ba420de 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandlerTests.cs @@ -22,7 +22,7 @@ namespace Microsoft.IdentityModel.Tokens.Saml.Tests /// /// /// - public class SamlSecurityTokenHandlerTests + public partial class SamlSecurityTokenHandlerTests { [Fact] public void Constructors()