Skip to content

Commit

Permalink
ValidateTokenAsync - New Path: Refactor result types (#2794)
Browse files Browse the repository at this point in the history
* Adding benchmark for new ValidateTokenAsync model vs old.

* Update benchmark/Microsoft.IdentityModel.Benchmarks/ValidateTokenAsyncWithVPTests.cs

* Removed IdentityComparer methods relating to removed result types. Updated tests for Algorithm and Audience validation

* Removed ITokenValidationError interface. Updated tests

* Replaced TokenValidationError with ExceptionDetails. Added stack frames to failures, added initial cache experiment for stack frames

* Removed optionality from CancellationToken parameter

* Updated tests

* Added ClaimsIdentity to ValidationResult

* Updated benchmarks to match the result types and added CancellationToken

* Removed test consoleapp, re-grouped benchmarks for better comparison

* Removed unit type

* Removed TokenValidationError since it is no longer used

* Restored ExceptionType type name

* Restored ExceptionFromType method in ExceptionDetail

* Override Result's empty initialiser and annotate it to prevent wrong initialisation

* Removed commented code.

* Commented empty if statement

* Added cached stack frames and nullability annotations for ValidateTokenAsync
Changed return type to use Result and removed error information from ValidationResult
Added method to add StackFrames to ExceptionDetail

* Added stack frames for ReadToken and DecryptToken

* Added ValidationFailureType to ExceptionDetail

* Updated tests to use ValidationFailureType

* Update src/Microsoft.IdentityModel.Abstractions/Result.cs

Co-authored-by: Keegan Caruso <[email protected]>

* Moved Result to Tokens project, made it internal. Reverted TargetFrameworks change to LoggingExtensions

* Addressed PR feedback around argument null exceptions

* Addressed PR comments. Removed unused imports. Increased epsilon for datetime comparison in tests.

---------

Co-authored-by: Franco Fung <[email protected]>
Co-authored-by: Franco Fung <[email protected]>
Co-authored-by: Keegan Caruso <[email protected]>

Add benchmark to test validation with an issuer delegate using string vs bytes issuer.

Add a class with profiler methods.
  • Loading branch information
iNinja authored and HP712 committed Sep 9, 2024
1 parent 8b61ba8 commit 065f1e7
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 17 deletions.
70 changes: 70 additions & 0 deletions benchmark/Microsoft.IdentityModel.Benchmarks/ProfilerRuns.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.Benchmarks
{
public class ProfilerRuns
{
ReadOnlyMemory<char> _encodedJWSAsMemory;
private JsonWebTokenHandler _jsonWebTokenHandler;
private SecurityTokenDescriptor _tokenDescriptorExtendedClaims;
private string _jwsExtendedClaims;
private TokenValidationParameters _tokenValidationParameters;

public ProfilerRuns()
{
var jsonWebTokenHandler = new JsonWebTokenHandler();
var jwsTokenDescriptor = new SecurityTokenDescriptor
{
SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256,
TokenType = JwtHeaderParameterNames.Jwk,
Claims = BenchmarkUtils.Claims
};

var encodedJWS = jsonWebTokenHandler.CreateToken(jwsTokenDescriptor);
_encodedJWSAsMemory = encodedJWS.AsMemory();

_tokenDescriptorExtendedClaims = new SecurityTokenDescriptor
{
Claims = BenchmarkUtils.ClaimsExtendedExample,
SigningCredentials = BenchmarkUtils.SigningCredentialsRsaSha256,
};

_jsonWebTokenHandler = new JsonWebTokenHandler();
_jwsExtendedClaims = _jsonWebTokenHandler.CreateToken(_tokenDescriptorExtendedClaims);

_tokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = BenchmarkUtils.Audience,
ValidateLifetime = true,
ValidIssuer = BenchmarkUtils.Issuer,
IssuerSigningKey = BenchmarkUtils.SigningCredentialsRsaSha256.Key,
};
}

public void ReadJws()
{
JsonWebToken jwt;

for (int i = 0; i < 1000; i++)
{
jwt = new JsonWebToken(_encodedJWSAsMemory);
}
}

public async Task ValidateJws()
{
TokenValidationResult tokenValidationResult;

for (int i = 0; i < 1000; i++)
{
tokenValidationResult = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParameters).ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#if NET8_0_OR_GREATER
using System;
#endif
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
Expand All @@ -27,10 +30,26 @@ public class ValidateTokenAsyncTests
private string _jws;
private string _jwsExtendedClaims;
private TokenValidationParameters _tokenValidationParameters;
private TokenValidationParameters _tokenValidationParametersValidateStringIssuer;
private TokenValidationParameters _tokenValidationParametersValidateBytesIssuer;
private TokenValidationParameters _invalidTokenValidationParameters;
private ValidationParameters _validationParameters;
private ValidationParameters _invalidValidationParameters;

private static ValueTask<string> IssuerValidatorCompareString(string issuer, SecurityToken token, TokenValidationParameters validationParameters)
{
var isValid = string.Equals(((JsonWebToken)token).Issuer, validationParameters.ValidIssuer);
return new ValueTask<string>(issuer);
}

private static ValueTask<string> IssuerValidatorCompareBytes(string issuer, SecurityToken token, TokenValidationParameters validationParameters)
{
#if NET8_0_OR_GREATER
var isValid = ((JsonWebToken)token).IssuerBytes.SequenceEqual(validationParameters.ValidIssuerBytes.Span);
#endif
return new ValueTask<string>(issuer);
}

[GlobalSetup]
public void Setup()
{
Expand Down Expand Up @@ -61,6 +80,24 @@ public void Setup()
IssuerSigningKey = BenchmarkUtils.SigningCredentialsRsaSha256.Key,
};

_tokenValidationParametersValidateStringIssuer = new TokenValidationParameters()
{
ValidAudience = BenchmarkUtils.Audience,
ValidateLifetime = true,
ValidIssuer = BenchmarkUtils.Issuer,
IssuerSigningKey = BenchmarkUtils.SigningCredentialsRsaSha256.Key,
IssuerValidatorAsync = IssuerValidatorCompareString,
};

_tokenValidationParametersValidateBytesIssuer = new TokenValidationParameters()
{
ValidAudience = BenchmarkUtils.Audience,
ValidateLifetime = true,
ValidIssuer = BenchmarkUtils.Issuer,
IssuerSigningKey = BenchmarkUtils.SigningCredentialsRsaSha256.Key,
IssuerValidatorAsync = IssuerValidatorCompareBytes,
};

_validationParameters = new ValidationParameters();
_validationParameters.ValidAudiences.Add(BenchmarkUtils.Audience);
_validationParameters.ValidIssuers.Add(BenchmarkUtils.Issuer);
Expand All @@ -83,13 +120,19 @@ public void Setup()
_callContext = new CallContext();
}

[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark]
[Benchmark(Baseline = true)]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncCompareStringIssuer() => await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParametersValidateStringIssuer).ConfigureAwait(false);

[Benchmark]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncCompareByteIssuer() => await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParametersValidateBytesIssuer).ConfigureAwait(false);

//[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark]
public async Task<TokenValidationResult> JwtSecurityTokenHandler_ValidateTokenAsync() => await _jwtSecurityTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParameters).ConfigureAwait(false);

[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark(Baseline = true)]
//[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark(Baseline = true)]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncWithTVP() => await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParameters).ConfigureAwait(false);

[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark]
//[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncWithTVPUsingModifiedClone()
{
var tokenValidationParameters = _tokenValidationParameters.Clone();
Expand All @@ -99,7 +142,7 @@ public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncW
return await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, tokenValidationParameters).ConfigureAwait(false);
}

[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark]
//[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark]
public async Task<bool> JsonWebTokenHandler_ValidateTokenAsyncWithVP()
{
// Because ValidationResult is an internal type, we cannot return it in the benchmark.
Expand All @@ -108,7 +151,7 @@ public async Task<bool> JsonWebTokenHandler_ValidateTokenAsyncWithVP()
return result.IsSuccess;
}

[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark(Baseline = true)]
//[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark(Baseline = true)]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncWithTVP_SucceedOnThirdAttempt()
{
TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters).ConfigureAwait(false);
Expand All @@ -118,7 +161,7 @@ public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncW
return result;
}

[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark]
//[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncWithTVPUsingClone_SucceedOnThirdAttempt()
{
TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters.Clone()).ConfigureAwait(false);
Expand All @@ -128,7 +171,7 @@ public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncW
return result;
}

[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark]
//[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark]
public async Task<bool> JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnThirdAttempt()
{
ValidationResult<ValidatedToken> result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidValidationParameters, _callContext, CancellationToken.None).ConfigureAwait(false);
Expand All @@ -138,7 +181,7 @@ public async Task<bool> JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnTh
return result.IsSuccess;
}

[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark(Baseline = true)]
//[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark(Baseline = true)]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncWithTVP_SucceedOnFifthAttempt()
{
TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters).ConfigureAwait(false);
Expand All @@ -150,7 +193,7 @@ public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncW
return result;
}

[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark]
//[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark]
public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncWithTVPUsingClone_SucceedOnFifthAttempt()
{
TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters.Clone()).ConfigureAwait(false);
Expand All @@ -162,7 +205,7 @@ public async Task<TokenValidationResult> JsonWebTokenHandler_ValidateTokenAsyncW
return result;
}

[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark]
//[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark]
public async Task<bool> JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnFifthAttempt()
{
ValidationResult<ValidatedToken> result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidValidationParameters, _callContext, CancellationToken.None).ConfigureAwait(false);
Expand All @@ -174,7 +217,7 @@ public async Task<bool> JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnFi
return result.IsSuccess;
}

[BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark(Baseline = true)]
//[BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark(Baseline = true)]
public async Task<List<Claim>> JsonWebTokenHandler_ValidateTokenAsyncWithTVP_CreateClaims()
{
var result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParameters).ConfigureAwait(false);
Expand All @@ -183,7 +226,7 @@ public async Task<List<Claim>> JsonWebTokenHandler_ValidateTokenAsyncWithTVP_Cre
return claims.ToList();
}

[BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark]
//[BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark]
public async Task<List<Claim>> JsonWebTokenHandler_ValidateTokenAsyncWithVP_CreateClaims()
{
ValidationResult<ValidatedToken> result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _validationParameters, _callContext, CancellationToken.None).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ namespace Microsoft.IdentityModel.JsonWebTokens
/// <remarks>This partial class contains methods and logic related to the validation of tokens' signatures.</remarks>
public partial class JsonWebTokenHandler : TokenHandler
{
static internal class SignatureStackFrames
{
// Test StackFrame to validate caching solution. Need to add all the possible stack frames.
static internal StackFrame? NoKeysProvided;
}
/// <summary>
/// Validates the JWT signature.
/// </summary>
Expand Down Expand Up @@ -110,8 +115,8 @@ internal static ValidationResult<SecurityKey> ValidateSignature(
return new ValidationError(
new MessageDetail(TokenLogMessages.IDX10500),
ValidationFailureType.SignatureValidationFailed,
typeof(SecurityTokenSignatureKeyNotFoundException),
noKeysProvidedStackFrame);
ExceptionType.SecurityTokenSignatureKeyNotFound,
stackFrame);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.IdentityModel.Tokens;

#nullable enable

namespace Microsoft.IdentityModel.JsonWebTokens
{
public partial class JsonWebTokenHandler : TokenHandler
Expand Down
15 changes: 14 additions & 1 deletion src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Abstractions;
using Microsoft.IdentityModel.Logging;

Expand Down Expand Up @@ -719,11 +720,23 @@ public string RoleClaimType
/// </summary>
public IEnumerable<string> ValidAudiences { get; set; }

private string _validIssuer;

/// <summary>
/// Gets or sets a <see cref="string"/> that represents a valid issuer that will be used to check against the token's issuer.
/// The default is <c>null</c>.
/// </summary>
public string ValidIssuer { get; set; }
public string ValidIssuer
{
get => _validIssuer;
set
{
_validIssuer = value;
ValidIssuerBytes = value != null ? Encoding.UTF8.GetBytes(value) : null;
}
}

internal ReadOnlyMemory<byte> ValidIssuerBytes { get; private set; }

/// <summary>
/// Gets or sets the <see cref="IEnumerable{String}"/> that contains valid issuers that will be used to check against the token's issuer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ protected virtual void AddAdditionalInformation(Exception exception)
/// </summary>
public ValidationFailureType FailureType { get; }

internal static ExceptionDetail NullParameter(string parameterName, StackFrame stackFrame) => new ExceptionDetail(
MessageDetail.NullParameter(parameterName),
ValidationFailureType.NullArgument,
ExceptionType.ArgumentNull, stackFrame);

/// <summary>
/// Gets the type of validation failure that occurred.
/// </summary>
public ValidationFailureType FailureType { get; }

/// <summary>
/// Gets the type of exception that occurred.
/// </summary>
Expand Down Expand Up @@ -149,4 +159,31 @@ public ValidationError AddStackFrame(StackFrame stackFrame)
return this;
}
}

internal enum ExceptionType
{
Unknown = -1,
ArgumentNull,
InvalidArgument,
InvalidOperation,
SecurityToken,
SecurityTokenDecompressionFailed,
SecurityTokenDecryptionFailed,
SecurityTokenExpired,
SecurityTokenInvalidAudience,
SecurityTokenInvalidAlgorithm,
SecurityTokenInvalidIssuer,
SecurityTokenInvalidLifetime,
SecurityTokenInvalidSigningKey,
SecurityTokenInvalidSignature,
SecurityTokenInvalidType,
SecurityTokenKeyWrap,
SecurityTokenMalformed,
SecurityTokenNoExpiration,
SecurityTokenNotYetValid,
SecurityTokenReplayDetected,
SecurityTokenReplayAddFailed,
SecurityTokenSignatureKeyNotFound,
ExceptionTypeCount
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable enable
using System;

#nullable enable
namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,6 @@ internal static async Task<ValidationResult<ValidatedIssuer>> ValidateIssuerAsyn
new StackFrame(true));
}
}
#nullable restore
}
#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt.Tests;
using Microsoft.IdentityModel.Abstractions;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.TestUtils;
using Microsoft.IdentityModel.Tokens;
Expand Down
Loading

0 comments on commit 065f1e7

Please sign in to comment.