Skip to content

Commit

Permalink
Validate Issuer Using New Validation Model in Saml2SecurityTokenHandl…
Browse files Browse the repository at this point in the history
…er (#2929)

* Initial changes to include Issuer validation to Saml2SecurityTokenHandler

* Clean-up

* Cache issuer validation failed stackframe

* Use WsFedConfig instead

* Addressing PR feedback

* Update src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs

Co-authored-by: kellyyangsong <[email protected]>

---------

Co-authored-by: Franco Fung <[email protected]>
Co-authored-by: Ignacio Inglese <[email protected]>
Co-authored-by: kellyyangsong <[email protected]>
  • Loading branch information
4 people authored Oct 23, 2024
1 parent 3ac0fb3 commit 92a00d9
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 6 deletions.
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
29 changes: 29 additions & 0 deletions src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs
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.
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

0 comments on commit 92a00d9

Please sign in to comment.