Skip to content

Commit

Permalink
Validate Audience for SAML2TokenHandler with New Model (#2863)
Browse files Browse the repository at this point in the history
* Initial test to check for painpoints integrating Wilson validation model for SAML

* Added more test cases and removed duplicate skipDelegates.cs

* removed failing non-applicable test

* Address PR feedback part 1

* Added new ValidateTokeAsync method to InternalAPI.Unshipped.txt

* Clean up PR feedback

* Simplify test into comparisson only tests.

* Update test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandlerTests.ValidateTokenAsyncTests.Audience.cs

Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com>

* Addressing PR feedback

* Addressing PR feedback

---------

Co-authored-by: Franco Fung <francofung@microsoft.com>
Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 14, 2024
1 parent 2347f09 commit 3dab668
Show file tree
Hide file tree
Showing 7 changed files with 440 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Microsoft.IdentityModel.Tokens.TokenValidationParameters>
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
Expand All @@ -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<Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidatedConditions>
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<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A <see cref="SecurityTokenHandler"/> designed for creating and validating Saml2 Tokens. See: http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
/// </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)
{
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<ValidatedConditions> 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<string> but returned as ICollection<string>
// no conversion occurs, ToList() is never called but we have to account for the possibility.
if (audienceRestriction.Audiences is not List<string> 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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Microsoft.IdentityModel.Tokens.Saml2
/// <summary>
/// A <see cref="SecurityTokenHandler"/> designed for creating and validating Saml2 Tokens. See: http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
/// </summary>
public class Saml2SecurityTokenHandler : SecurityTokenHandler
public partial class Saml2SecurityTokenHandler : SecurityTokenHandler
{
private const string _actor = "Actor";
private const string _className = "Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler";
Expand Down Expand Up @@ -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<ValidatedConditions> 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<string> but returned as ICollection<string>
// no conversion occurs, ToList() is never called but we have to account for the possibility.
if (!(audienceRestriction.Audiences is List<string> 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

/// <summary>
/// Validates the OneTimeUse condition.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Loading

0 comments on commit 3dab668

Please sign in to comment.