Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate Issuer Using New Validation Model in Saml2SecurityTokenHandler #2929

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
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.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationParameters>
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<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,34 @@ internal static async Task<TokenValidationParameters> PopulateValidationParamete
return validationParametersCloned;

}

/// <summary>
/// Fetches current configuration from the ConfigurationManager of <paramref name="validationParameters"/>
/// and populates ValidIssuers and IssuerSigningKeys.
/// </summary>
/// <param name="validationParameters"> the token validation parameters to update.</param>
/// <param name="cancellationToken"></param>
/// <returns> New ValidationParameters with ValidIssuers and IssuerSigningKeys updated.</returns>
internal static async Task<ValidationParameters> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
FuPingFranco marked this conversation as resolved.
Show resolved Hide resolved
throw LogArgumentNullException(nameof(value));

_value = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,15 +15,11 @@ namespace Microsoft.IdentityModel.Tokens.Saml2
/// </summary>
public partial class Saml2SecurityTokenHandler : SecurityTokenHandler
{
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
internal async Task<ValidationResult<ValidatedToken>> 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)
{
Expand All @@ -40,14 +37,32 @@ internal async Task<ValidationResult<ValidatedToken>> 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)
{
StackFrames.AssertionConditionsValidationFailed ??= new StackFrame(true);
return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed);
}

ValidationResult<ValidatedIssuer> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ValidatedToken> 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<ValidateTokenAsyncIssuerTheoryData> ValidateTokenAsync_IssuerTestCases
{
get
{
var theoryData = new TheoryData<ValidateTokenAsyncIssuerTheoryData>();

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<WsFederationConfiguration>(validConfig);
}

return tokenValidationParameters;
}
}
}
#nullable restore
Loading