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

Regression tests: Audience #2838

Merged
merged 15 commits into from
Oct 1, 2024
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
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that since we are creating derived ValidationError types, we do not need the method AddAdditionalInformation or the interface ISecurityTokenException.

For example, when we create the AudienceValidationError, we call the ctor that takes the list of available audiences.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s keep the conversation going on that to find the best solution that can support any exception our users may decide to return.
I’d rather not expand the scope of this PR to that as it’s already a week in review.

{
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
iNinja marked this conversation as resolved.
Show resolved Hide resolved
{
[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")
iNinja marked this conversation as resolved.
Show resolved Hide resolved
{
// 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")
{
iNinja marked this conversation as resolved.
Show resolved Hide resolved
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:"),
iNinja marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should this be a valid Audience while the Audiences property on TVP or VP is null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current validation path considers the case where no valid audiences are provided to be invalid and throws.
The new validation path took that as a base and kept the same behaviour.

If you believe that having no valid audiences should be considered the same as skipping the validation, we can look into it and find out what the correct behaviour would be.

ExpectedIsValid = false,
// TVP path has a special case when ValidAudience is null or empty and ValidAudiences is null.
ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"),
FuPingFranco marked this conversation as resolved.
Show resolved Hide resolved
// VP path has a default empty List for ValidAudiences, so it will always return IDX10206 if no audiences are provided.
iNinja marked this conversation as resolved.
Show resolved Hide resolved
ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenInvalidAudienceException("IDX10206:"),
iNinja marked this conversation as resolved.
Show resolved Hide resolved
},
};

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

// Only validate the audience.
new TokenValidationParameters
iNinja marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading