diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index 5396e123a7..4978a5a0a6 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.SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task Microsoft.IdentityModel.Tokens.Saml2.SamlSecurityTokenHandler.ValidateTokenAsync(SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame @@ -22,6 +23,7 @@ static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AudienceValidationFailed -> System.Diagnostics.StackFrame +static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.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.TokenNull -> System.Diagnostics.StackFrame diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs index 31ded481de..525edc6e21 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs @@ -154,5 +154,34 @@ internal static async Task PopulateValidationParamete return validationParametersCloned; } + + /// + /// Fetches current configuration from the ConfigurationManager of + /// and populates ValidIssuers and IssuerSigningKeys. + /// + /// the token validation parameters to update. + /// + /// New ValidationParameters with ValidIssuers and IssuerSigningKeys updated. + internal static async Task PopulateValidationParametersWithCurrentConfigurationAsync( + ValidationParameters validationParameters, + CancellationToken cancellationToken) + { + if (validationParameters.ConfigurationManager == null) + { + return validationParameters; + } + + var currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(cancellationToken).ConfigureAwait(false); + var validationParametersCloned = validationParameters.Clone(); + + validationParametersCloned.ValidIssuers.Add(currentConfiguration.Issuer); + + foreach (SecurityKey key in currentConfiguration.SigningKeys) + { + validationParametersCloned.IssuerSigningKeys.Add(key); + } + + return validationParametersCloned; + } } } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2NameIdentifier.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2NameIdentifier.cs index 408f3b4151..112fcebbcf 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2NameIdentifier.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2NameIdentifier.cs @@ -104,7 +104,7 @@ public string Value get { return _value; } set { - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrEmpty(value)) //NOTE: We can remove this check and let our issuer validator handle this. throw LogArgumentNullException(nameof(value)); _value = value; 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 2bb12a27e5..0d23b4644f 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens.Saml; #nullable enable namespace Microsoft.IdentityModel.Tokens.Saml2 @@ -14,15 +15,11 @@ namespace Microsoft.IdentityModel.Tokens.Saml2 /// 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) { @@ -40,7 +37,12 @@ internal async Task> ValidateTokenAsync( StackFrames.TokenValidationParametersNull); } - var conditionsResult = ValidateConditions(samlToken, validationParameters, callContext); + validationParameters = await SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(validationParameters, cancellationToken).ConfigureAwait(false); + + var conditionsResult = ValidateConditions( + samlToken, + validationParameters, + callContext); if (!conditionsResult.IsValid) { @@ -48,6 +50,19 @@ internal async Task> ValidateTokenAsync( return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed); } + ValidationResult validatedIssuerResult = await validationParameters.IssuerValidatorAsync( + samlToken.Issuer, + samlToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (!validatedIssuerResult.IsValid) + { + StackFrames.IssuerValidationFailed ??= new StackFrame(true); + return validatedIssuerResult.UnwrapError().AddStackFrame(StackFrames.IssuerValidationFailed); + } + return new ValidatedToken(samlToken, this, validationParameters); } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs index 65c3bed36a..dcd253940a 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? IssuerValidationFailed; internal static StackFrame? AssertionConditionsValidationFailed; internal static StackFrame? LifetimeValidationFailed; internal static StackFrame? OneTimeUseValidationFailed; diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Issuer.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Issuer.cs new file mode 100644 index 0000000000..0246ca0046 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Issuer.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.WsFederation; +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_IssuerTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_IssuerComparison(ValidateTokenAsyncIssuerTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_IssuerComparison", theoryData); + + var saml2Token = CreateTokenWithIssuer(theoryData.TokenIssuer); + + var tokenValidationParameters = CreateTokenValidationParametersForIssuerValidationOnly( + saml2Token, + theoryData.NullTokenValidationParameters, + theoryData.ValidationParametersIssuer, + theoryData.ConfigurationIssuer); + + Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); + + // Validate token using TokenValidationParameters + TokenValidationResult tokenValidationResult = + await saml2TokenHandler.ValidateTokenAsync(saml2Token.Assertion.CanonicalString, tokenValidationParameters); + + // Validate token using ValidationParameters. + ValidationResult validationResult = + await saml2TokenHandler.ValidateTokenAsync( + saml2Token, + 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_IssuerTestCases + { + get + { + var theoryData = new TheoryData(); + + theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Valid_IssuerIsValidIssuer") + { + TokenIssuer = Default.Issuer, + ValidationParametersIssuer = Default.Issuer, + ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Valid_IssuerIsConfigurationIssuer") + { + TokenIssuer = Default.Issuer, + ConfigurationIssuer = Default.Issuer, + ValidationParameters = CreateValidationParameters(configurationIssuer: Default.Issuer), + }); + + theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Invalid_IssuerIsNotValid") + { + TokenIssuer = "InvalidIssuer", + ValidationParametersIssuer = Default.Issuer, + ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer), + ExpectedIsValid = false, + ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10205:"), + ExpectedExceptionValidationParameters = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10212:") + }); + + theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Invalid_IssuerIsWhitespace") + { + //This test will cover the case where the issuer is null or empty as well since, we do not allow tokens to be created with null or empty issuer. + TokenIssuer = " ", + ValidationParametersIssuer = Default.Issuer, + ValidationParameters = CreateValidationParameters(validIssuer: Default.Issuer), + ExpectedIsValid = false, + ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10211:") + }); + + theoryData.Add(new ValidateTokenAsyncIssuerTheoryData("Invalid_NoValidIssuersProvided") + { + TokenIssuer = Default.Issuer, + ValidationParametersIssuer = string.Empty, + ValidationParameters = CreateValidationParameters(), + ExpectedIsValid = false, + ExpectedException = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10204:"), + ExpectedExceptionValidationParameters = new ExpectedException(typeof(SecurityTokenInvalidIssuerException), "IDX10211:") + }); + + return theoryData; + + static ValidationParameters CreateValidationParameters( + string? validIssuer = null, + string? configurationIssuer = null) + { + ValidationParameters validationParameters = new ValidationParameters(); + + // Skip all validations except issuer + validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation; + validationParameters.AudienceValidator = SkipValidationDelegates.SkipAudienceValidation; + validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation; + validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation; + validationParameters.SignatureValidator = SkipValidationDelegates.SkipSignatureValidation; + + return validationParameters; + } + } + } + + public class ValidateTokenAsyncIssuerTheoryData : TheoryDataBase + { + public ValidateTokenAsyncIssuerTheoryData(string testId) : base(testId) { } + + internal ValidationParameters? ValidationParameters { get; set; } + + internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = ExpectedException.NoExceptionExpected; + + internal bool ExpectedIsValid { get; set; } = true; + + public bool NullTokenValidationParameters { get; internal set; } = false; + + public string? TokenIssuer { get; set; } + + public string? ValidationParametersIssuer { get; set; } = null; + + public string? ConfigurationIssuer { get; set; } = null; + } + + private static Saml2SecurityToken CreateTokenWithIssuer(string? issuer) + { + Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler(); + + SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = Default.AsymmetricSigningCredentials, + Audience = Default.Audience, + Issuer = issuer, + Subject = Default.SamlClaimsIdentity + }; + + return (Saml2SecurityToken)saml2TokenHandler.CreateToken(securityTokenDescriptor); + } + + private static TokenValidationParameters? CreateTokenValidationParametersForIssuerValidationOnly( + Saml2SecurityToken saml2SecurityToken, + bool nullTokenValidationParameters, + string? validIssuer, + string? configurationIssuer) + { + if (nullTokenValidationParameters) + { + return null; + } + + var tokenValidationParameters = new TokenValidationParameters() + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateTokenReplay = false, + ValidateIssuerSigningKey = false, + IssuerSigningKey = Default.AsymmetricSigningKey, + ValidAudiences = [Default.Audience], + ValidIssuer = validIssuer, + SignatureValidator = delegate (string token, TokenValidationParameters validationParameters) + { + return saml2SecurityToken; + } + }; + + if (configurationIssuer is not null) + { + var validConfig = new WsFederationConfiguration() { Issuer = configurationIssuer }; + tokenValidationParameters.ConfigurationManager = new MockConfigurationManager(validConfig); + } + + return tokenValidationParameters; + } + } +} +#nullable restore