Skip to content

Commit

Permalink
Regression tests: Audience (#2838)
Browse files Browse the repository at this point in the history
* Fixed audience validator unit tests. Renamed variables to improve clarity.

* Extracted regression tests related to audience to a separate file. Added remaining regression tests for audience scenarios.

* Remove whitespace

* Cherry picked common functionality extraction from Lifetime branch

* Added AudienceValidationError to handle custom exception properties

* Added delegates to skip validations

* Removed token signing from audience validation tests, skipping all validations but audience

* Updated validation delegate names

* Updated header being written in tests

* Moved skip validation delegates to TestUtils

* Added DisableDiscoveryEnumeration flag to test method

* Moved concatenation of strings used when creating Audience related exceptions out of the constructor
  • Loading branch information
iNinja authored Oct 1, 2024
1 parent f6c8f02 commit 8b4eea6
Show file tree
Hide file tree
Showing 8 changed files with 606 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;

#nullable enable
namespace Microsoft.IdentityModel.Tokens
{
internal class AudienceValidationError : ValidationError
{
private IList<string>? _invalidAudiences;

public AudienceValidationError(
MessageDetail messageDetail,
Type exceptionType,
StackFrame stackFrame,
IList<string>? invalidAudiences)
: base(messageDetail, ValidationFailureType.AudienceValidationFailed, exceptionType, stackFrame)
{
_invalidAudiences = invalidAudiences;
}

internal override void AddAdditionalInformation(ISecurityTokenException exception)
{
if (exception is SecurityTokenInvalidAudienceException invalidAudienceException)
invalidAudienceException.InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(_invalidAudiences);
}
}
}
#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -52,31 +52,31 @@ internal static ValidationResult<string> ValidateAudience(IList<string> tokenAud
new StackFrame(true));

if (tokenAudiences == null)
return new ValidationError(
return new AudienceValidationError(
new MessageDetail(LogMessages.IDX10207),
ValidationFailureType.AudienceValidationFailed,
typeof(SecurityTokenInvalidAudienceException),
new StackFrame(true));
new StackFrame(true),
tokenAudiences);

if (tokenAudiences.Count == 0)
return new ValidationError(
return new AudienceValidationError(
new MessageDetail(LogMessages.IDX10206),
ValidationFailureType.AudienceValidationFailed,
typeof(SecurityTokenInvalidAudienceException),
new StackFrame(true));
new StackFrame(true),
tokenAudiences);

string? validAudience = ValidTokenAudience(tokenAudiences, validationParameters.ValidAudiences, validationParameters.IgnoreTrailingSlashWhenValidatingAudience);
if (validAudience != null)
return validAudience;

return new ValidationError(
return new AudienceValidationError(
new MessageDetail(
LogMessages.IDX10215,
LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(tokenAudiences)),
LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences))),
ValidationFailureType.AudienceValidationFailed,
typeof(SecurityTokenInvalidAudienceException),
new StackFrame(true));
new StackFrame(true),
tokenAudiences);
}

private static string? ValidTokenAudience(IList<string> tokenAudiences, IList<string> validAudiences, bool ignoreTrailingSlashWhenValidatingAudience)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.IdentityModel.TestUtils;
using Microsoft.IdentityModel.Tokens;
using Xunit;

namespace Microsoft.IdentityModel.JsonWebTokens.Tests
{
public partial class JsonWebTokenHandlerValidateTokenAsyncTests
{
[Theory, MemberData(nameof(ValidateTokenAsync_AudienceTestCases), DisableDiscoveryEnumeration = true)]
public async Task ValidateTokenAsync_Audience(ValidateTokenAsyncAudienceTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync_Audience", theoryData);

string jwtString = CreateToken(theoryData.Audience);

await ValidateAndCompareResults(jwtString, theoryData, context);

TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<ValidateTokenAsyncAudienceTheoryData> ValidateTokenAsync_AudienceTestCases
{
get
{
return new TheoryData<ValidateTokenAsyncAudienceTheoryData>
{
new ValidateTokenAsyncAudienceTheoryData("Valid_AudiencesMatch")
{
Audience = Default.Audience,
TokenValidationParameters = CreateTokenValidationParameters([Default.Audience]),
ValidationParameters = CreateValidationParameters([Default.Audience]),
},
new ValidateTokenAsyncAudienceTheoryData("Invalid_AudiencesDontMatch")
{
// This scenario is the same if the token audience is an empty string or whitespace.
// As long as the token audience and the valid audience are not equal, the validation fails.
TokenValidationParameters = CreateTokenValidationParameters([Default.Audience]),
ValidationParameters = CreateValidationParameters([Default.Audience]),
Audience = "InvalidAudience",
ExpectedIsValid = false,
ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"),
// ValidateTokenAsync with ValidationParameters returns a different error message to account for the
// removal of the ValidAudience property from the ValidationParameters class.
ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10215:"),
},
new ValidateTokenAsyncAudienceTheoryData("Valid_AudienceWithinValidAudiences")
{
Audience = Default.Audience,
TokenValidationParameters = CreateTokenValidationParameters(["ExtraAudience", Default.Audience, "AnotherAudience"]),
ValidationParameters = CreateValidationParameters(["ExtraAudience", Default.Audience, "AnotherAudience"]),
},
new ValidateTokenAsyncAudienceTheoryData("Valid_AudienceWithSlash_IgnoreTrailingSlashTrue")
{
// Audience has a trailing slash, but IgnoreTrailingSlashWhenValidatingAudience is true.
Audience = Default.Audience + "/",
TokenValidationParameters = CreateTokenValidationParameters([Default.Audience], true),
ValidationParameters = CreateValidationParameters([Default.Audience], true),
},
new ValidateTokenAsyncAudienceTheoryData("Invalid_AudienceWithSlash_IgnoreTrailingSlashFalse")
{
// Audience has a trailing slash and IgnoreTrailingSlashWhenValidatingAudience is false.
Audience = Default.Audience + "/",
TokenValidationParameters = CreateTokenValidationParameters([Default.Audience], false),
ValidationParameters = CreateValidationParameters([Default.Audience], false),
ExpectedIsValid = false,
ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"),
ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10215:"),
},
new ValidateTokenAsyncAudienceTheoryData("Valid_ValidAudiencesWithSlash_IgnoreTrailingSlashTrue")
{
// ValidAudiences has a trailing slash, but IgnoreTrailingSlashWhenValidatingAudience is true.
Audience = Default.Audience,
TokenValidationParameters = CreateTokenValidationParameters([Default.Audience + "/"], true),
ValidationParameters = CreateValidationParameters([Default.Audience + "/"], true),
},
new ValidateTokenAsyncAudienceTheoryData("Invalid_ValidAudiencesWithSlash_IgnoreTrailingSlashFalse")
{
// ValidAudiences has a trailing slash and IgnoreTrailingSlashWhenValidatingAudience is false.
Audience = Default.Audience,
TokenValidationParameters = CreateTokenValidationParameters([Default.Audience + "/"], false),
ValidationParameters = CreateValidationParameters([Default.Audience + "/"], false),
ExpectedIsValid = false,
ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"),
ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10215:"),
},
new ValidateTokenAsyncAudienceTheoryData("Invalid_AudienceNullIsTreatedAsEmptyList")
{
// JsonWebToken.Audiences defaults to an empty list if no audiences are provided.
TokenValidationParameters = CreateTokenValidationParameters([Default.Audience]),
ValidationParameters = CreateValidationParameters([Default.Audience]),
Audience = null,
ExpectedIsValid = false,
ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10206:"),
},
new ValidateTokenAsyncAudienceTheoryData("Invalid_ValidAudiencesIsNull")
{
TokenValidationParameters = CreateTokenValidationParameters(null),
ValidationParameters = CreateValidationParameters(null),
Audience = string.Empty,
ExpectedIsValid = false,
// TVP path has a special case when ValidAudience is null or empty and ValidAudiences is null.
ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"),
// VP path has a default empty List for ValidAudiences, so it will always return IDX10206 if no audiences are provided.
ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10206:"),
},
};

static TokenValidationParameters CreateTokenValidationParameters(
List<string>? audiences,
bool ignoreTrailingSlashWhenValidatingAudience = false) =>

// Only validate the audience.
new TokenValidationParameters
{
ValidateAudience = true,
ValidateIssuer = false,
ValidateLifetime = false,
ValidateTokenReplay = false,
ValidateIssuerSigningKey = false,
RequireSignedTokens = false,
ValidAudiences = audiences,
IgnoreTrailingSlashWhenValidatingAudience = ignoreTrailingSlashWhenValidatingAudience,
};

static ValidationParameters CreateValidationParameters(
List<string>? audiences,
bool ignoreTrailingSlashWhenValidatingAudience = false)
{
ValidationParameters validationParameters = new ValidationParameters();
audiences?.ForEach(audience => validationParameters.ValidAudiences.Add(audience));
validationParameters.IgnoreTrailingSlashWhenValidatingAudience = ignoreTrailingSlashWhenValidatingAudience;

// Skip all validations except audience
validationParameters.AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation;
validationParameters.IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation;
validationParameters.IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation;
validationParameters.LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation;
validationParameters.SignatureValidator = SkipValidationDelegates.SkipSignatureValidation;

return validationParameters;
}
}
}

public class ValidateTokenAsyncAudienceTheoryData : ValidateTokenAsyncBaseTheoryData
{
public ValidateTokenAsyncAudienceTheoryData(string testId) : base(testId) { }

public string? Audience { get; internal set; } = Default.Audience;
}

private static string CreateToken(string? audience)
{
JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler();

SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor
{
Subject = Default.ClaimsIdentity,
Audience = audience,
};

return jsonWebTokenHandler.CreateToken(securityTokenDescriptor);
}
}
}
#nullable restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable enable
using System.Threading.Tasks;
using System.Threading;
using Microsoft.IdentityModel.TestUtils;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.JsonWebTokens.Tests
{
public partial class JsonWebTokenHandlerValidateTokenAsyncTests
{
internal static async Task ValidateAndCompareResults(
string jwtString,
ValidateTokenAsyncBaseTheoryData theoryData,
CompareContext context)
{
JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler();

// Validate the token using TokenValidationParameters
TokenValidationResult legacyTokenValidationParametersResult =
await jsonWebTokenHandler.ValidateTokenAsync(jwtString, theoryData.TokenValidationParameters);

// Validate the token using ValidationParameters
ValidationResult<ValidatedToken> validationParametersResult =
await jsonWebTokenHandler.ValidateTokenAsync(
jwtString, theoryData.ValidationParameters!, theoryData.CallContext, CancellationToken.None);

// Ensure the validity of the results match the expected result
if (legacyTokenValidationParametersResult.IsValid != theoryData.ExpectedIsValid)
context.AddDiff($"tokenValidationParametersResult.IsValid != theoryData.ExpectedIsValid");

if (validationParametersResult.IsSuccess != theoryData.ExpectedIsValid)
context.AddDiff($"validationParametersResult.IsSuccess != theoryData.ExpectedIsValid");

if (theoryData.ExpectedIsValid &&
legacyTokenValidationParametersResult.IsValid &&
validationParametersResult.IsSuccess)
{
// Compare the ClaimsPrincipal and ClaimsIdentity from one result against the other
IdentityComparer.AreEqual(
legacyTokenValidationParametersResult.ClaimsIdentity,
validationParametersResult.UnwrapResult().ClaimsIdentity,
context);
IdentityComparer.AreEqual(
legacyTokenValidationParametersResult.Claims,
validationParametersResult.UnwrapResult().Claims,
context);
}
else
{
// Verify the exception provided by the TokenValidationParameters path
theoryData.ExpectedException.ProcessException(legacyTokenValidationParametersResult.Exception, context);

if (!validationParametersResult.IsSuccess)
{
// Verify the exception provided by the ValidationParameters path
if (theoryData.ExpectedExceptionValidationParameters is not null)
{
// If there is a special case for the ValidationParameters path, use that.
theoryData.ExpectedExceptionValidationParameters
.ProcessException(validationParametersResult.UnwrapError().GetException(), context);
}
else
{
theoryData.ExpectedException
.ProcessException(validationParametersResult.UnwrapError().GetException(), context);

// If the expected exception is the same in both paths, verify the message matches
IdentityComparer.AreStringsEqual(
legacyTokenValidationParametersResult.Exception.Message,
validationParametersResult.UnwrapError().GetException().Message,
context);
}
}

// Verify that the exceptions are of the same type.
IdentityComparer.AreEqual(
legacyTokenValidationParametersResult.Exception.GetType(),
validationParametersResult.UnwrapError().GetException().GetType(),
context);

if (legacyTokenValidationParametersResult.Exception is SecurityTokenException)
{
// Verify that the custom properties are the same.
IdentityComparer.AreSecurityTokenExceptionsEqual(
legacyTokenValidationParametersResult.Exception,
validationParametersResult.UnwrapError().GetException(),
context);
}
}
}
}

public class ValidateTokenAsyncBaseTheoryData : TheoryDataBase
{
public ValidateTokenAsyncBaseTheoryData(string testId) : base(testId) { }

internal bool ExpectedIsValid { get; set; } = true;

internal TokenValidationParameters? TokenValidationParameters { get; set; }

internal ValidationParameters? ValidationParameters { get; set; }

// only set if we expect a different message on this path
internal ExpectedException? ExpectedExceptionValidationParameters { get; set; } = null;
}
}
#nullable restore
Loading

0 comments on commit 8b4eea6

Please sign in to comment.