-
Notifications
You must be signed in to change notification settings - Fork 409
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
8 changed files
with
606 additions
and
112 deletions.
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AudienceValidationError.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
...IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Audience.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
110 changes: 110 additions & 0 deletions
110
...t.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Common.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.