diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index ff31950f14..247f47da39 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -216,6 +216,7 @@ Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.AttributeKey.Orig Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.AttributeKey.ValueType.get -> string Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.Equals(Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.AttributeKey x, Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.AttributeKey y) -> bool Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.GetHashCode(Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.AttributeKey obj) -> int +Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidatedConditions Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidatedConditions.ValidatedAudience.get -> string Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidatedConditions.ValidatedAudience.set -> void @@ -243,6 +244,13 @@ static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.GetXsiTypeForValue static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters) -> System.Threading.Tasks.Task static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.ResolveTokenSigningKey(Microsoft.IdentityModel.Xml.KeyInfo tokenKeyInfo, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters) -> Microsoft.IdentityModel.Tokens.SecurityKey static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.IsSaml2Assertion(System.Xml.XmlReader reader) -> bool +static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsNull -> 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 static Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.CanCreateValidUri(string uriString, System.UriKind uriKind) -> bool static Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.LogReadException(string format, params object[] args) -> System.Exception static Microsoft.IdentityModel.Tokens.Saml2.Saml2Serializer.LogReadException(string format, System.Exception inner, params object[] args) -> System.Exception @@ -260,3 +268,4 @@ static readonly Microsoft.IdentityModel.Tokens.Saml2.Saml2AttributeKeyComparer.I static readonly Microsoft.IdentityModel.Tokens.Saml2.Saml2AuthorizationDecisionStatement.EmptyResource -> System.Uri virtual Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateConditions(Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult virtual Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateProxyRestriction(Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError +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> diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs new file mode 100644 index 0000000000..7918058462 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml2 +{ + /// + /// A designed for creating and validating Saml2 Tokens. See: http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf + /// + public partial class Saml2SecurityTokenHandler : 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 + Saml2SecurityToken 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.IsSuccess) + { + return conditionsResult.UnwrapError().AddStackFrame(new StackFrame(true)); + } + + 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(Saml2SecurityToken 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.IsSuccess) + { + StackFrames.LifetimeValidationFailed ??= new StackFrame(true); + return lifetimeValidationResult.UnwrapError().AddStackFrame(StackFrames.LifetimeValidationFailed); + } + + if (samlToken.Assertion.Conditions.OneTimeUse) + { + //ValidateOneTimeUseCondition(samlToken, validationParameters); + // We can keep an overridable method for this, or rely on the TokenReplayValidator delegate. + var oneTimeUseValidationResult = validationParameters.TokenReplayValidator( + samlToken.Assertion.Conditions.NotOnOrAfter, + samlToken.Assertion.CanonicalString, + validationParameters, + callContext); + + if (!oneTimeUseValidationResult.IsSuccess) + { + StackFrames.OneTimeUseValidationFailed ??= new StackFrame(true); + return oneTimeUseValidationResult.UnwrapError().AddStackFrame(StackFrames.OneTimeUseValidationFailed); + } + } + + if (samlToken.Assertion.Conditions.ProxyRestriction != null) + { + //throw LogExceptionMessage(new SecurityTokenValidationException(LogMessages.IDX13511)); + var proxyValidationError = ValidateProxyRestriction( + samlToken, + validationParameters, + callContext); + + if (proxyValidationError is not null) + { + return proxyValidationError; + } + } + + string? validatedAudience = null; + foreach (var audienceRestriction in samlToken.Assertion.Conditions.AudienceRestrictions) + { + // AudienceRestriction.Audiences is a List but returned as ICollection + // no conversion occurs, ToList() is never called but we have to account for the possibility. + if (audienceRestriction.Audiences is not List audiencesAsList) + audiencesAsList = [.. audienceRestriction.Audiences]; + + var audienceValidationResult = validationParameters.AudienceValidator( + audiencesAsList, + samlToken, + validationParameters, + callContext); + if (!audienceValidationResult.IsSuccess) + return audienceValidationResult.UnwrapError(); + + // Audience is valid, save it for later. + validatedAudience = audienceValidationResult.UnwrapResult(); + } + + return new ValidatedConditions(validatedAudience, lifetimeValidationResult.UnwrapResult()); + } + +#pragma warning disable CA1801 // Review unused parameters + internal virtual ValidationError? ValidateProxyRestriction(Saml2SecurityToken samlToken, ValidationParameters validationParameters, CallContext callContext) +#pragma warning restore CA1801 // Review unused parameters + { + // return an error, or ignore and allow overriding? + return null; + } + } +} +#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 new file mode 100644 index 0000000000..61466f8406 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml2 +{ + public partial class Saml2SecurityTokenHandler : 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? LifetimeValidationFailed; + internal static StackFrame? OneTimeUseValidationFailed; + } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs index da190bc158..9e2ad28749 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs @@ -22,7 +22,7 @@ namespace Microsoft.IdentityModel.Tokens.Saml2 /// /// A designed for creating and validating Saml2 Tokens. See: http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf /// - public class Saml2SecurityTokenHandler : SecurityTokenHandler + public partial class Saml2SecurityTokenHandler : SecurityTokenHandler { private const string _actor = "Actor"; private const string _className = "Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler"; @@ -1048,99 +1048,6 @@ protected virtual void ValidateConditions(Saml2SecurityToken samlToken, TokenVal throw LogExceptionMessage(new Saml2SecurityTokenException(LogMessages.IDX13002)); } -#nullable enable - // 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) - { - if (samlToken == null) - return ValidationError.NullParameter(nameof(samlToken), new System.Diagnostics.StackFrame(true)); - - if (validationParameters == null) - return ValidationError.NullParameter(nameof(validationParameters), new System.Diagnostics.StackFrame(true)); - - if (samlToken.Assertion == null) - return ValidationError.NullParameter(nameof(samlToken.Assertion), new System.Diagnostics.StackFrame(true)); - - // TokenValidationParameters.RequireAudience is only used for SAML. - // Should we add this to ValidationParameters? - // Should it be just a field in Saml2SecurityTokenHandler? - bool requireAudience = true; - - if (samlToken.Assertion.Conditions == null) - { - if (requireAudience) - return new ValidationError( - new MessageDetail(LogMessages.IDX13002), - ValidationFailureType.AudienceValidationFailed, - typeof(Saml2SecurityTokenException), - new System.Diagnostics.StackFrame(true)); - - return new ValidatedConditions(null, null); // no error occurred. There is no validated audience or lifetime. - } - - var lifetimeValidationResult = validationParameters.LifetimeValidator( - samlToken.Assertion.Conditions.NotBefore, samlToken.Assertion.Conditions.NotOnOrAfter, samlToken, validationParameters, callContext); - if (!lifetimeValidationResult.IsSuccess) - return lifetimeValidationResult.UnwrapError(); - - if (samlToken.Assertion.Conditions.OneTimeUse) - { - //ValidateOneTimeUseCondition(samlToken, validationParameters); - // We can keep an overridable method for this, or rely on the TokenReplayValidator delegate. - var oneTimeUseValidationResult = validationParameters.TokenReplayValidator( - samlToken.Assertion.Conditions.NotOnOrAfter, samlToken.Assertion.CanonicalString, validationParameters, callContext); - if (!oneTimeUseValidationResult.IsSuccess) - return oneTimeUseValidationResult.UnwrapError(); - } - - if (samlToken.Assertion.Conditions.ProxyRestriction != null) - { - //throw LogExceptionMessage(new SecurityTokenValidationException(LogMessages.IDX13511)); - var proxyValidationError = ValidateProxyRestriction(samlToken, validationParameters, callContext); - if (proxyValidationError is not null) - return proxyValidationError; - } - - string? validatedAudience = null; - foreach (var audienceRestriction in samlToken.Assertion.Conditions.AudienceRestrictions) - { - // AudienceRestriction.Audiences is a List but returned as ICollection - // no conversion occurs, ToList() is never called but we have to account for the possibility. - if (!(audienceRestriction.Audiences is List audiencesAsList)) - audiencesAsList = audienceRestriction.Audiences.ToList(); - - var audienceValidationResult = validationParameters.AudienceValidator( - audiencesAsList, samlToken, validationParameters, callContext); - if (!audienceValidationResult.IsSuccess) - return audienceValidationResult.UnwrapError(); - - // Audience is valid, save it for later. - validatedAudience = audienceValidationResult.UnwrapResult(); - } - - if (requireAudience && validatedAudience is null) - { - return new ValidationError( - new MessageDetail(LogMessages.IDX13002), - ValidationFailureType.AudienceValidationFailed, - typeof(Saml2SecurityTokenException), - new System.Diagnostics.StackFrame(true)); - } - - return new ValidatedConditions(validatedAudience, lifetimeValidationResult.UnwrapResult()); // no error occurred. There is nothing else to return. - } - -#pragma warning disable CA1801 // Review unused parameters - internal virtual ValidationError? ValidateProxyRestriction(Saml2SecurityToken samlToken, ValidationParameters validationParameters, CallContext callContext) -#pragma warning restore CA1801 // Review unused parameters - { - // return an error, or ignore and allow overriding? - return null; - } -#nullable restore - /// /// Validates the OneTimeUse condition. /// diff --git a/test/Microsoft.IdentityModel.TestUtils/InternalsVisibleTo.cs b/test/Microsoft.IdentityModel.TestUtils/InternalsVisibleTo.cs index 84597b7355..0cb96bb49b 100644 --- a/test/Microsoft.IdentityModel.TestUtils/InternalsVisibleTo.cs +++ b/test/Microsoft.IdentityModel.TestUtils/InternalsVisibleTo.cs @@ -3,3 +3,4 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.JsonWebTokens.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Tokens.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Tokens.Saml.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs new file mode 100644 index 0000000000..a6c0905388 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs @@ -0,0 +1,251 @@ +// 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_Audience_TestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_AudienceComparison(ValidateTokenAsyncAudienceTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_AudienceComparison", theoryData); + + Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); + + var saml2Token = CreateToken(theoryData.TokenAudience!, theoryData.Saml2Condition!); + + var tokenValidationParameters = CreateTokenValidationParameters( + theoryData.TVPAudiences, + saml2Token, + theoryData.NullTokenValidationParameters, + theoryData.IgnoreTrailingSlashWhenValidatingAudience); + + // Validate the token using TokenValidationParameters + TokenValidationResult tokenValidationResult = + await saml2TokenHandler.ValidateTokenAsync(saml2Token.Assertion.CanonicalString, 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.IsSuccess) + { + 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") + { + ValidationParameters = CreateValidationParameters([Default.Audience]), + TokenAudience = "InvalidAudience", + TVPAudiences = [Default.Audience], + ExpectedIsValid = false, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10215:"), + }); + + theoryData.Add(new ValidateTokenAsyncAudienceTheoryData("Invalid_TokenAudienceIsWhiteSpace") + { + ValidationParameters = CreateValidationParameters([Default.Audience]), + TokenAudience = " ", + 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 Saml2Conditions? Saml2Condition { get; internal set; } + + public string? TokenAudience { get; internal set; } + + public List? TVPAudiences { get; internal set; } + } + + private static Saml2SecurityToken CreateToken(string audience, Saml2Conditions saml2Conditions) + { + Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + Expires = DateTime.UtcNow + TimeSpan.FromDays(1), + Audience = audience, + SigningCredentials = Default.AsymmetricSigningCredentials, + Issuer = Default.Issuer, + Subject = Default.SamlClaimsIdentity + }; + + Saml2SecurityToken saml2Token = (Saml2SecurityToken)saml2TokenHandler.CreateToken(securityTokenDescriptor); + + return saml2Token; + } + + private static TokenValidationParameters? CreateTokenValidationParameters( + List? audiences, + Saml2SecurityToken saml2SecurityToken, + 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 saml2SecurityToken; + }, + RequireAudience = true + }; + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs index 6cb2ad5c94..94f5bfcb2a 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.IdentityModel.Tokens.Saml2.Tests { - public class Saml2SecurityTokenHandlerTests + public partial class Saml2SecurityTokenHandlerTests { [Fact] public void Constructors()