Skip to content

Commit

Permalink
Implement and Test Audience and Lifetime validations in SamlSecurityT…
Browse files Browse the repository at this point in the history
…okenHandler with New Validation Model (#2925)

* Implement and test Audience and Lifetime validations in SamlSecurityTokenHandler using new validation model

* Removed unesserasary method declaration in InternalAPI.Unshipped.txt file

* Addressing PR feedback. Cached condition validation filure stackFrame

* Clean-up

---------

Co-authored-by: Franco Fung <[email protected]>
Co-authored-by: Ignacio Inglese <[email protected]>
  • Loading branch information
3 people authored Oct 22, 2024
1 parent 2858319 commit 69b15a7
Show file tree
Hide file tree
Showing 11 changed files with 685 additions and 9 deletions.
20 changes: 20 additions & 0 deletions src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedAudience.get -> string
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedAudience.set -> void
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedConditions() -> void
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedConditions(string ValidatedAudience, Microsoft.IdentityModel.Tokens.ValidatedLifetime? ValidatedLifetime) -> void
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedLifetime.get -> Microsoft.IdentityModel.Tokens.ValidatedLifetime?
Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions.ValidatedLifetime.set -> void
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>>
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
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AudienceValidationFailed -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.LifetimeValidationFailed -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.OneTimeUseValidationFailed -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenNull -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenValidationParametersNull -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.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.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
virtual Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateConditions(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidatedConditions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

#nullable enable
namespace Microsoft.IdentityModel.Tokens.Saml
{
/// <summary>
/// A <see cref="SecurityTokenHandler"/> designed for creating and validating Saml Tokens. See: http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
/// </summary>
public partial class SamlSecurityTokenHandler : 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
SamlSecurityToken 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.IsValid)
{
StackFrames.AssertionConditionsValidationFailed ??= new StackFrame(true);
return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed);
}

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(
SamlSecurityToken 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.IsValid)
{
StackFrames.LifetimeValidationFailed ??= new StackFrame(true);
return lifetimeValidationResult.UnwrapError().AddStackFrame(StackFrames.LifetimeValidationFailed);
}

string? validatedAudience = null;
foreach (var condition in samlToken.Assertion.Conditions.Conditions)
{

if (condition is SamlAudienceRestrictionCondition audienceRestriction)
{

// AudienceRestriction.Audiences is an ICollection<Uri> so we need make a conversion to List<string> before calling our audience validator
var audiencesAsList = audienceRestriction.Audiences.Select(static x => x.OriginalString).ToList();

var audienceValidationResult = validationParameters.AudienceValidator(
audiencesAsList,
samlToken,
validationParameters,
callContext);

if (!audienceValidationResult.IsValid)
return audienceValidationResult.UnwrapError();

validatedAudience = audienceValidationResult.UnwrapResult();
}

if (validatedAudience != null)
break;
}

return new ValidatedConditions(validatedAudience, lifetimeValidationResult.UnwrapResult());
}
}
}
#nullable restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics;

#nullable enable
namespace Microsoft.IdentityModel.Tokens.Saml
{
public partial class SamlSecurityTokenHandler : 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? AssertionConditionsValidationFailed;
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.Saml
/// which supports validating tokens passed as strings using <see cref="TokenValidationParameters"/>.
/// </summary>
///
public class SamlSecurityTokenHandler : SecurityTokenHandler
public partial class SamlSecurityTokenHandler : SecurityTokenHandler
{
internal const string Actor = "Actor";
private const string _className = "Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ internal async Task<ValidationResult<ValidatedToken>> ValidateTokenAsync(

if (!conditionsResult.IsValid)
{
return conditionsResult.UnwrapError().AddStackFrame(new StackFrame(true));
StackFrames.AssertionConditionsValidationFailed ??= new StackFrame(true);
return conditionsResult.UnwrapError().AddStackFrame(StackFrames.AssertionConditionsValidationFailed);
}

return new ValidatedToken(samlToken, this, validationParameters);
Expand All @@ -53,7 +54,10 @@ internal async Task<ValidationResult<ValidatedToken>> ValidateTokenAsync(
// 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)
internal virtual ValidationResult<ValidatedConditions> ValidateConditions(
Saml2SecurityToken samlToken,
ValidationParameters validationParameters,
CallContext callContext)
{
if (samlToken.Assertion is null)
{
Expand Down Expand Up @@ -129,7 +133,10 @@ internal virtual ValidationResult<ValidatedConditions> ValidateConditions(Saml2S
validationParameters,
callContext);
if (!audienceValidationResult.IsValid)
return audienceValidationResult.UnwrapError();
{
StackFrames.AudienceValidationFailed ??= new StackFrame(true);
return audienceValidationResult.UnwrapError().AddStackFrame(StackFrames.AudienceValidationFailed);
}

// Audience is valid, save it for later.
validatedAudience = audienceValidationResult.UnwrapResult();
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? AssertionConditionsValidationFailed;
internal static StackFrame? LifetimeValidationFailed;
internal static StackFrame? OneTimeUseValidationFailed;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public async Task ValidateTokenAsync_AudienceComparison(ValidateTokenAsyncAudien

Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler();

var saml2Token = CreateToken(theoryData.TokenAudience!, theoryData.Saml2Condition!);
var saml2Token = CreateTokenForAudienceValidation(theoryData.TokenAudience!, theoryData.Saml2Condition!);

var tokenValidationParameters = CreateTokenValidationParameters(
theoryData.TVPAudiences,
Expand Down Expand Up @@ -200,7 +200,7 @@ public ValidateTokenAsyncAudienceTheoryData(string testId) : base(testId) { }
public List<string>? TVPAudiences { get; internal set; }
}

private static Saml2SecurityToken CreateToken(string audience, Saml2Conditions saml2Conditions)
private static Saml2SecurityToken CreateTokenForAudienceValidation(string audience, Saml2Conditions saml2Conditions)
{
Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public async Task ValidateTokenAsync_LifetimeComparison(ValidateTokenAsyncLifeti
{
var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_LifetimeComparison", theoryData);

var saml2Token = CreateToken(
var saml2Token = CreateTokenForLifetimeValidation(
theoryData.IssuedAt,
theoryData.NotBefore,
theoryData.Expires);
Expand Down Expand Up @@ -209,7 +209,7 @@ public ValidateTokenAsyncLifetimeTheoryData(string testId) : base(testId) { }
public bool NullTokenValidationParameters { get; internal set; } = false;
}

private static Saml2SecurityToken CreateToken(DateTime? issuedAt, DateTime? notBefore, DateTime? expires)
private static Saml2SecurityToken CreateTokenForLifetimeValidation(DateTime? issuedAt, DateTime? notBefore, DateTime? expires)
{
Saml2SecurityTokenHandler saml2TokenHandler = new Saml2SecurityTokenHandler();

Expand Down
Loading

0 comments on commit 69b15a7

Please sign in to comment.