Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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,213 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable enable
using System.Collections.Generic;
using System.Threading;
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))]
public async Task ValidateTokenAsync_Audience(ValidateTokenAsyncAudienceTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.ValidateTokenAsync", theoryData);

JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler();

SecurityTokenDescriptor securityTokenDescriptor = new SecurityTokenDescriptor
{
Subject = Default.ClaimsIdentity,
SigningCredentials = Default.AsymmetricSigningCredentials,
Audience = theoryData.Audience,
Issuer = Default.Issuer,
};

string jwtString = jsonWebTokenHandler.CreateToken(securityTokenDescriptor);

TokenValidationResult tokenValidationParametersResult =
await jsonWebTokenHandler.ValidateTokenAsync(jwtString, theoryData.TokenValidationParameters);
ValidationResult<ValidatedToken> validationParametersResult =
await jsonWebTokenHandler.ValidateTokenAsync(
jwtString, theoryData.ValidationParameters!, theoryData.CallContext, CancellationToken.None);

if (tokenValidationParametersResult.IsValid != theoryData.ExpectedIsValid)
context.AddDiff($"tokenValidationParametersResult.IsValid != theoryData.ExpectedIsValid");

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

if (theoryData.ExpectedIsValid &&
tokenValidationParametersResult.IsValid &&
validationParametersResult.IsSuccess)
{
IdentityComparer.AreEqual(
tokenValidationParametersResult.ClaimsIdentity,
validationParametersResult.UnwrapResult().ClaimsIdentity,
context);
IdentityComparer.AreEqual(
tokenValidationParametersResult.Claims,
validationParametersResult.UnwrapResult().Claims,
context);
}
else
{
theoryData.ExpectedException.ProcessException(tokenValidationParametersResult.Exception, context);

if (!validationParametersResult.IsSuccess)
{
// If there is a special case for the ValidationParameters path, use that.
if (theoryData.ExpectedExceptionValidationParameters != null)
theoryData.ExpectedExceptionValidationParameters
.ProcessException(validationParametersResult.UnwrapError().GetException(), context);
else
theoryData.ExpectedException
.ProcessException(validationParametersResult.UnwrapError().GetException(), 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,
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:"),
// 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) =>

new TokenValidationParameters
{
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateTokenReplay = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = Default.AsymmetricSigningKey,
ValidAudiences = audiences,
ValidIssuer = Default.Issuer,
IgnoreTrailingSlashWhenValidatingAudience = ignoreTrailingSlashWhenValidatingAudience,
};

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

return validationParameters;
}
}
}

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

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

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