diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/ProfilerRuns.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/ProfilerRuns.cs new file mode 100644 index 0000000000..608f19ede6 --- /dev/null +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/ProfilerRuns.cs @@ -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 _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); + } + } + } +} diff --git a/benchmark/Microsoft.IdentityModel.Benchmarks/ValidateTokenAsyncTests.cs b/benchmark/Microsoft.IdentityModel.Benchmarks/ValidateTokenAsyncTests.cs index b174a691dd..a22285e9fe 100644 --- a/benchmark/Microsoft.IdentityModel.Benchmarks/ValidateTokenAsyncTests.cs +++ b/benchmark/Microsoft.IdentityModel.Benchmarks/ValidateTokenAsyncTests.cs @@ -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; @@ -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 IssuerValidatorCompareString(string issuer, SecurityToken token, TokenValidationParameters validationParameters) + { + var isValid = string.Equals(((JsonWebToken)token).Issuer, validationParameters.ValidIssuer); + return new ValueTask(issuer); + } + + private static ValueTask 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(issuer); + } + [GlobalSetup] public void Setup() { @@ -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); @@ -83,13 +120,19 @@ public void Setup() _callContext = new CallContext(); } - [BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark] + [Benchmark(Baseline = true)] + public async Task JsonWebTokenHandler_ValidateTokenAsyncCompareStringIssuer() => await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParametersValidateStringIssuer).ConfigureAwait(false); + + [Benchmark] + public async Task JsonWebTokenHandler_ValidateTokenAsyncCompareByteIssuer() => await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParametersValidateBytesIssuer).ConfigureAwait(false); + + //[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark] public async Task JwtSecurityTokenHandler_ValidateTokenAsync() => await _jwtSecurityTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParameters).ConfigureAwait(false); - [BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark(Baseline = true)] + //[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark(Baseline = true)] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithTVP() => await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParameters).ConfigureAwait(false); - [BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark] + //[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithTVPUsingModifiedClone() { var tokenValidationParameters = _tokenValidationParameters.Clone(); @@ -99,7 +142,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncW return await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, tokenValidationParameters).ConfigureAwait(false); } - [BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark] + //[BenchmarkCategory("ValidateTokenAsync_Success"), Benchmark] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithVP() { // Because ValidationResult is an internal type, we cannot return it in the benchmark. @@ -108,7 +151,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncWithVP() return result.IsSuccess; } - [BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark(Baseline = true)] + //[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark(Baseline = true)] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithTVP_SucceedOnThirdAttempt() { TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters).ConfigureAwait(false); @@ -118,7 +161,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncW return result; } - [BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark] + //[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithTVPUsingClone_SucceedOnThirdAttempt() { TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters.Clone()).ConfigureAwait(false); @@ -128,7 +171,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncW return result; } - [BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark] + //[BenchmarkCategory("ValidateTokenAsync_FailTwiceBeforeSuccess"), Benchmark] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnThirdAttempt() { Result result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidValidationParameters, _callContext, CancellationToken.None).ConfigureAwait(false); @@ -138,7 +181,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnTh return result.IsSuccess; } - [BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark(Baseline = true)] + //[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark(Baseline = true)] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithTVP_SucceedOnFifthAttempt() { TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters).ConfigureAwait(false); @@ -150,7 +193,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncW return result; } - [BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark] + //[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithTVPUsingClone_SucceedOnFifthAttempt() { TokenValidationResult result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidTokenValidationParameters.Clone()).ConfigureAwait(false); @@ -162,7 +205,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncW return result; } - [BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark] + //[BenchmarkCategory("ValidateTokenAsync_FailFourTimesBeforeSuccess"), Benchmark] public async Task JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnFifthAttempt() { Result result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _invalidValidationParameters, _callContext, CancellationToken.None).ConfigureAwait(false); @@ -174,7 +217,7 @@ public async Task JsonWebTokenHandler_ValidateTokenAsyncWithVP_SucceedOnFi return result.IsSuccess; } - [BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark(Baseline = true)] + //[BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark(Baseline = true)] public async Task> JsonWebTokenHandler_ValidateTokenAsyncWithTVP_CreateClaims() { var result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _tokenValidationParameters).ConfigureAwait(false); @@ -183,7 +226,7 @@ public async Task> JsonWebTokenHandler_ValidateTokenAsyncWithTVP_Cre return claims.ToList(); } - [BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark] + //[BenchmarkCategory("ValidateTokenAsyncClaimAccess"), Benchmark] public async Task> JsonWebTokenHandler_ValidateTokenAsyncWithVP_CreateClaims() { Result result = await _jsonWebTokenHandler.ValidateTokenAsync(_jwsExtendedClaims, _validationParameters, _callContext, CancellationToken.None).ConfigureAwait(false); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 7bcfb4d4ce..dd576d95e0 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -24,11 +24,12 @@ internal class JsonClaimSet internal object _claimsLock = new(); internal readonly Dictionary _jsonClaims; + private List _claims; internal JsonClaimSet() { - _jsonClaims = new Dictionary(); + _jsonClaims = []; } internal JsonClaimSet(Dictionary jsonClaims) @@ -49,8 +50,17 @@ internal List CreateClaims(string issuer) { var claims = new List(_jsonClaims.Count); foreach (KeyValuePair kvp in _jsonClaims) + { CreateClaimFromObject(claims, kvp.Key, kvp.Value, issuer); +#if NET8_0_OR_GREATER + if (kvp.Value is Memory bytes) + { + string value = System.Text.Encoding.UTF8.GetString(bytes.Span); + claims.Add(new Claim(kvp.Key, value, ClaimValueTypes.String, issuer, issuer)); + } +#endif + } return claims; } @@ -167,12 +177,32 @@ internal string GetStringValue(string key) if (obj == null) return null; +#if NET8_0_OR_GREATER + if (obj is Memory bytes) + return System.Text.Encoding.UTF8.GetString(bytes.Span); +#endif return obj.ToString(); } - return string.Empty; } +#if NET8_0_OR_GREATER + // Similar to GetStringValue but returns the bytes directly. + internal ReadOnlySpan GetStringBytesValue(string key) + { + if (_jsonClaims.TryGetValue(key, out object obj)) + { + if (obj == null) + return null; + + if (obj is Memory bytes) + return bytes.Span; + } + + return []; + } +#endif + internal DateTime GetDateTime(string key) { long l = GetValue(key, false, out bool found); @@ -235,8 +265,16 @@ internal T GetValue(string key, bool throwEx, out bool found) if (list.Count == 1) return (T)((object)(list[0])); } +#if NET8_0_OR_GREATER + else if (obj is Memory bytes) + { + return (T)(object)System.Text.Encoding.UTF8.GetString(bytes.Span); + } +#endif else + { return (T)((object)obj.ToString()); + } } else if (typeof(T) == typeof(bool)) { @@ -425,13 +463,24 @@ internal bool TryGetClaim(string key, string issuer, out Claim claim) /// if the key was found; otherwise, . internal bool TryGetValue(string key, out T value) { +#if NET8_0_OR_GREATER + if (typeof(T) == typeof(string)) + { + var span = GetStringBytesValue(key); + if (!span.IsEmpty) + { + value = (T)(object)System.Text.Encoding.UTF8.GetString(span); + return true; + } + } +#endif value = GetValue(key, false, out bool found); return found; } internal bool HasClaim(string claimName) { - return _jsonClaims.TryGetValue(claimName, out _); + return _jsonClaims.ContainsKey(claimName); } } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs index 4694399aa7..b77964093b 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs @@ -36,46 +36,13 @@ internal JsonClaimSet CreateHeaderClaimSet(ReadOnlySpan byteSpan) LogHelper.MarkAsNonPII(reader.CurrentDepth), LogHelper.MarkAsNonPII(reader.BytesConsumed)))); - Dictionary claims = new(); + Dictionary claims = []; while (true) { if (reader.TokenType == JsonTokenType.PropertyName) { - if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) - { - _alg = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); - claims[JwtHeaderParameterNames.Alg] = _alg; - } - else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Cty)) - { - _cty = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); - claims[JwtHeaderParameterNames.Cty] = _cty; - } - else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Kid)) - { - _kid = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); - claims[JwtHeaderParameterNames.Kid] = _kid; - } - else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Typ)) - { - _typ = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); - claims[JwtHeaderParameterNames.Typ] = _typ; - } - else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.X5t)) - { - _x5t = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); - claims[JwtHeaderParameterNames.X5t] = _x5t; - } - else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Zip)) - { - _zip = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); - claims[JwtHeaderParameterNames.Zip] = _zip; - } - else - { - string propertyName = reader.GetString(); - claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); - } + string claimName = reader.GetString(); + claims[claimName] = ReadTokenHeaderValueDelegate(ref reader, claimName); } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -86,5 +53,68 @@ internal JsonClaimSet CreateHeaderClaimSet(ReadOnlySpan byteSpan) return new JsonClaimSet(claims); } + + /// + /// Reads and saves the value of the header claim from the reader. + /// + /// The reader over the JWT. + /// The claim at the current position of the reader. + /// A claim that was read. + public static object ReadTokenHeaderValue(ref Utf8JsonReader reader, string claimName) + { +#if NET8_0_OR_GREATER + if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) + { + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Cty)) + { + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Kid)) + { + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Typ)) + { + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.X5t)) + { + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Zip)) + { + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); + } +#else + if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) + { + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Cty)) + { + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Kid)) + { + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Typ)) + { + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.X5t)) + { + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Zip)) + { + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); + } +#endif + + return JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, claimName, JsonClaimSet.ClassName, true); + } } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index dac70e195b..469db840ea 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text.Json; using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens.Json; namespace Microsoft.IdentityModel.JsonWebTokens @@ -36,11 +35,13 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) LogHelper.MarkAsNonPII(reader.BytesConsumed)))); Dictionary claims = []; + while (true) { if (reader.TokenType == JsonTokenType.PropertyName) { - ReadPayloadValue(ref reader, claims); + string claimName = reader.GetString(); + claims[claimName] = ReadTokenPayloadValueDelegate(ref reader, claimName); } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -52,73 +53,73 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) return new JsonClaimSet(claims); } - private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) + /// + /// Reads and saves the value of the payload claim from the reader. + /// + /// The reader over the JWT. + /// The claim at the current position of the reader. + /// A claim that was read. + public static object ReadTokenPayloadValue(ref Utf8JsonReader reader, string claimName) { if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { - _audiences = []; + List _audiences = []; reader.Read(); if (reader.TokenType == JsonTokenType.StartArray) { JsonSerializerPrimitives.ReadStringsSkipNulls(ref reader, _audiences, JwtRegisteredClaimNames.Aud, ClassName); - claims[JwtRegisteredClaimNames.Aud] = _audiences; } else { if (reader.TokenType != JsonTokenType.Null) { _audiences.Add(JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Aud, ClassName)); - claims[JwtRegisteredClaimNames.Aud] = _audiences[0]; - } - else - { - claims[JwtRegisteredClaimNames.Aud] = _audiences; } } + return _audiences; } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) { - _azp = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); - claims[JwtRegisteredClaimNames.Azp] = _azp; +#if NET8_0_OR_GREATER + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); +#else + return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); +#endif } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Exp)) { - _exp = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true); - _expDateTime = EpochTime.DateTime(_exp.Value); - claims[JwtRegisteredClaimNames.Exp] = _exp; + return JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true); } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iat)) { - _iat = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true); - _iatDateTime = EpochTime.DateTime(_iat.Value); - claims[JwtRegisteredClaimNames.Iat] = _iat; + return JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true); } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss)) { - _iss = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); - claims[JwtRegisteredClaimNames.Iss] = _iss; +#if NET8_0_OR_GREATER + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); +#else + return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); +#endif } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti)) { - _jti = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); - claims[JwtRegisteredClaimNames.Jti] = _jti; +#if NET8_0_OR_GREATER + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); +#else + return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); +#endif } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Nbf)) { - _nbf = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true); - _nbfDateTime = EpochTime.DateTime(_nbf.Value); - claims[JwtRegisteredClaimNames.Nbf] = _nbf; + return JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true); } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Sub)) { - _sub = JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true); - claims[JwtRegisteredClaimNames.Sub] = _sub; - } - else - { - string propertyName = reader.GetString(); - claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); + return JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true); } + + return JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, claimName, JsonClaimSet.ClassName, true); } } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 48af759d86..e95d2598ec 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -86,6 +86,29 @@ public JsonWebToken(string jwtEncodedString) _encodedToken = jwtEncodedString; } + /// + /// Initializes a new instance of from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format. + /// + /// A ReadOnlyMemory{char} containing the JSON Web Token serialized in JWS or JWE Compact format. + /// A custom delegate to be called when each header claim is being read. If null, default implementation is called. + /// A custom delegate to be called when each payload claim is being read. If null, default implementation is called. + public JsonWebToken( + ReadOnlyMemory encodedTokenMemory, + ReadTokenHeaderValueDelegate readTokenHeaderValueDelegate, + ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) + { + if (encodedTokenMemory.IsEmpty) + throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(encodedTokenMemory))); + + ReadTokenHeaderValueDelegate = readTokenHeaderValueDelegate ?? ReadTokenHeaderValue; + ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue; + + ReadToken(encodedTokenMemory); + + _encodedTokenMemory = encodedTokenMemory; + + } + /// /// Initializes a new instance of from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format. /// @@ -141,6 +164,43 @@ public JsonWebToken(string header, string payload) _encodedToken = encodedToken; } + /// + /// Called for each claim when token header is being read. + /// + /// + /// An example implementation: + /// + /// object ReadPayloadValueDelegate(ref Utf8JsonReader reader, string claimName) => + /// { + /// if (reader.ValueTextEquals("CustomProp")) + /// { + /// return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true); + /// } + /// return JsonWebToken.ReadTokenHeaderValue(ref reader, claimName); + /// } + /// + /// + internal ReadTokenHeaderValueDelegate ReadTokenHeaderValueDelegate { get; set; } = ReadTokenHeaderValue; + + + /// + /// Called for each claim when token payload is being read. + /// + /// + /// An example implementation: + /// + /// object ReadPayloadValueDelegate(ref Utf8JsonReader reader, string claimName) => + /// { + /// if (reader.ValueTextEquals("CustomProp")) + /// { + /// return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true); + /// } + /// return JsonWebToken.ReadTokenPayloadValue(ref reader, claimName); + /// } + /// + /// + internal ReadTokenPayloadValueDelegate ReadTokenPayloadValueDelegate { get; set; } = ReadTokenPayloadValue; + internal string ActualIssuer { get; set; } internal ClaimsIdentity ActorClaimsIdentity { get; set; } @@ -1139,5 +1199,44 @@ internal DateTime? ValidToNullable } } #endregion + +#if NET8_0_OR_GREATER + + #region Header Properties Bytes + + /// + public ReadOnlySpan AlgBytes => Payload.GetStringBytesValue(JwtHeaderParameterNames.Alg); + + /// + public ReadOnlySpan CtyBytes => Payload.GetStringBytesValue(JwtHeaderParameterNames.Cty); + + /// + public ReadOnlySpan KidBytes => Payload.GetStringBytesValue(JwtHeaderParameterNames.Kid); + + /// + public ReadOnlySpan TypBytes => Payload.GetStringBytesValue(JwtHeaderParameterNames.Typ); + + /// + public ReadOnlySpan X5tBytes => Payload.GetStringBytesValue(JwtHeaderParameterNames.X5t); + + /// + public ReadOnlySpan ZipBytes => Payload.GetStringBytesValue(JwtHeaderParameterNames.Zip); + + #endregion + + #region Payload Properties Bytes + + /// + public ReadOnlySpan AzpBytes => Payload.GetStringBytesValue(JwtRegisteredClaimNames.Azp); + + /// + public ReadOnlySpan IdBytes => Payload.GetStringBytesValue(JwtRegisteredClaimNames.Jti); + + /// + public ReadOnlySpan IssuerBytes => Payload.GetStringBytesValue(JwtRegisteredClaimNames.Iss); + + #endregion + +#endif } } diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 5bfcb566f8..4c13f1c802 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; namespace Microsoft.IdentityModel.Tokens @@ -214,4 +215,22 @@ namespace Microsoft.IdentityModel.Tokens /// The transformed . internal delegate SecurityToken TransformBeforeSignatureValidationDelegate(SecurityToken token, ValidationParameters validationParameters); #nullable restore + + /// + /// Definition for ReadTokenHeaderValueDelegate. + /// Called for each claim when token header is being read. + /// + /// Reader for the underlying token bytes. + /// The name of the claim being read. + /// + public delegate object ReadTokenHeaderValueDelegate(ref Utf8JsonReader reader, string claimName); + + /// + /// Definition for ReadTokenPayloadValueDelegate. + /// Called for each claim when token payload is being read. + /// + /// Reader for the underlying token bytes. + /// The name of the claim being read. + /// + public delegate object ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader, string claimName); } diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index db68b15015..2a6c0fedb5 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -653,6 +653,34 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName return retval; } +#if NET8_0_OR_GREATER + // Mostly the same as ReadString, but this method returns the position of the claim value in the token bytes. + // This method does not unescape the value. The JsonWebToken GetValue, etc. methods are responsible for unescaping the value. + internal static Memory? ReadStringBytes( + ref Utf8JsonReader reader, + string propertyName, + string className, + bool read = false) + { + // returning null keeps the same logic as JsonSerialization.ReadObject + if (IsReaderPositionedOnNull(ref reader, read, true)) + return null; + + if (!IsReaderAtTokenType(ref reader, JsonTokenType.String, false)) + throw LogHelper.LogExceptionMessage( + CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); + + + var stringBytes = new Memory(new byte[reader.ValueSpan.Length]); + reader.CopyString(stringBytes.Span); + + // Move to next token + reader.Read(); + + return stringBytes; + } +#endif + internal static string ReadStringAsBool(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) { // The parameter 'read' can be used by callers reader position the reader to the next token. diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index ab93e8f58a..5b8c7ad59c 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -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; @@ -449,6 +450,16 @@ public string NameClaimType /// public IDictionary PropertyBag { get; set; } + /// + /// Gets or sets a delegate that will be called when reading token payload claims. + /// + public ReadTokenHeaderValueDelegate ReadTokenHeaderValue { get; set; } + + /// + /// Gets or sets a delegate that will be called when reading token payload claims. + /// + public ReadTokenPayloadValueDelegate ReadTokenPayloadValue { get; set; } + /// /// Gets or sets a boolean to control if configuration required to be refreshed before token validation. /// @@ -709,11 +720,23 @@ public string RoleClaimType /// public IEnumerable ValidAudiences { get; set; } + private string _validIssuer; + /// /// Gets or sets a that represents a valid issuer that will be used to check against the token's issuer. /// The default is null. /// - public string ValidIssuer { get; set; } + public string ValidIssuer + { + get => _validIssuer; + set + { + _validIssuer = value; + ValidIssuerBytes = value != null ? Encoding.UTF8.GetBytes(value) : ReadOnlyMemory.Empty; + } + } + + internal ReadOnlyMemory ValidIssuerBytes { get; private set; } = ReadOnlyMemory.Empty; /// /// Gets or sets the that contains valid issuers that will be used to check against the token's issuer. diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs deleted file mode 100644 index 813b82e4b3..0000000000 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.IdentityModel.Tokens.Json; - -namespace Microsoft.IdentityModel.JsonWebTokens.Tests -{ - public class CustomJsonWebToken : JsonWebToken - { - private const string CustomClaimName = "CustomClaim"; - - public CustomJsonWebToken(string jwtEncodedString) : base(jwtEncodedString) { } - - public CustomJsonWebToken(ReadOnlyMemory encodedTokenMemory) : base(encodedTokenMemory) { } - - public CustomJsonWebToken(string header, string payload) : base(header, payload) { } - - private protected override void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) - { - if (reader.ValueTextEquals(CustomClaimName)) - { - _customClaim = JsonSerializerPrimitives.ReadString(ref reader, CustomClaimName, ClassName, true); - claims[CustomClaimName] = _customClaim; - } - else - { - base.ReadPayloadValue(ref reader, claims); - } - } - - private string _customClaim; - - public string CustomClaim - { - get - { - _customClaim ??= Payload.GetStringValue(CustomClaimName); - return _customClaim; - } - } - } -} diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 04e9902186..507c87839b 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -12,9 +12,11 @@ using System.Reflection; using System.Security.Claims; using System.Text; +using System.Text.Json; using System.Threading; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens.Json; using Microsoft.IdentityModel.Tokens.Json.Tests; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -1541,6 +1543,53 @@ public static TheoryData ParseTimeValuesTheoryData } } +#if NET8_0_OR_GREATER + [Fact] + public void ParseToken_EscapedAndUnescaped_PropertiesCorrectlySet() + { + var unescapedAzp = "AA\\AA"; + var unknownClaimName = "unknown_claim_name"; + var unknownClaimValue = "unknown_claim_value"; + + // CreateToken uses Utf8JsonWriter which correctly escapes strings. + var tokenStr = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor + { + TokenType = JwtHeaderParameterNames.Jwk, + Claims = new Dictionary() + { + { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(Default.Expires) }, + { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(Default.NotBefore) }, + { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant) }, + { JwtRegisteredClaimNames.Iss, Default.Issuer }, + { JwtRegisteredClaimNames.Aud, Default.Audience }, + { JwtRegisteredClaimNames.Azp, unescapedAzp }, + { JwtRegisteredClaimNames.Jti, Default.Jti }, + { unknownClaimName, unknownClaimValue }, + } + }); + + var jwt = new JsonWebToken(tokenStr); + + // Check known claim that doesn't need to be escaped. + Assert.True(jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Iss, out string issuerFromPayload)); + Assert.Equal(Default.Issuer, issuerFromPayload); + Assert.Equal(Default.Issuer, jwt.Issuer); + Assert.Equal(Default.Issuer, jwt.GetPayloadValue(JwtRegisteredClaimNames.Iss)); + Assert.True(jwt.IssuerBytes.SequenceEqual(Encoding.UTF8.GetBytes(Default.Issuer))); + + // Check known claim that needs to be escaped. + Assert.True(jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Azp, out string azpFromPayload)); + Assert.Equal(unescapedAzp, azpFromPayload); + Assert.Equal(unescapedAzp, jwt.Azp); + Assert.Equal(unescapedAzp, jwt.GetPayloadValue(JwtRegisteredClaimNames.Azp)); + Assert.True(jwt.AzpBytes.SequenceEqual(Encoding.UTF8.GetBytes(unescapedAzp))); + + // Check unknown claim that doesn't need to be escaped. + Assert.True(jwt.TryGetPayloadValue(unknownClaimName, out string unknownClaimValueFromPayload)); + Assert.Equal(unknownClaimValue, unknownClaimValueFromPayload); + } +#endif + // Test ensures that we only try to populate a JsonWebToken from a string if it is a properly formatted JWT. // More specifically, we only want to try and decode // a JWT token if it has the correct number of (JWE or JWS) token parts. @@ -1741,22 +1790,55 @@ public void StringAndMemoryConstructors_CreateEquivalentTokens(JwtTheoryData the } [Fact] - public void DerivedJsonWebToken_IsCreatedCorrectly() + public void ReadTokenDelegates_CalledCorrectly() { - var expectedCustomClaim = "customclaim"; - var tokenStr = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor + var tokenSpan = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor { Issuer = Default.Issuer, Claims = new Dictionary { - { "CustomClaim", expectedCustomClaim }, + { "CustomPayload", "custom_payload" }, + }, + AdditionalHeaderClaims = new Dictionary + { + { "CustomHeader", "custom_header" } } - }); + }).AsMemory(); + + object ReadHeaderValue(ref Utf8JsonReader reader, string claimName) + { + if (reader.ValueTextEquals("CustomHeader"u8)) + { + return new CustomHeaderClaim(JsonSerializerPrimitives.ReadString(ref reader, "CustomHeader", string.Empty, true)); + } + return JsonWebToken.ReadTokenHeaderValue(ref reader, claimName); + } - var derivedToken = new CustomJsonWebToken(tokenStr); + object ReadPayloadValue(ref Utf8JsonReader reader, string claimName) + { + if (reader.ValueTextEquals("CustomPayload"u8)) + { + return new CustomPayloadClaim(JsonSerializerPrimitives.ReadString(ref reader, "CustomPayload", string.Empty, true)); + } + return JsonWebToken.ReadTokenPayloadValue(ref reader, claimName); + } - Assert.Equal(expectedCustomClaim, derivedToken.CustomClaim); - Assert.Equal(Default.Issuer, derivedToken.Issuer); + var jwt = new JsonWebToken(tokenSpan, ReadHeaderValue, ReadPayloadValue); + + Assert.True(jwt.TryGetHeaderValue("CustomHeader", out var actualHeaderClaim)); + Assert.True(jwt.TryGetPayloadValue("CustomPayload", out var actualPayloadClaim)); + + Assert.Equal("custom_header", actualHeaderClaim.CustomValue); + Assert.Equal("custom_payload", actualPayloadClaim.CustomValue); + } + + private class CustomHeaderClaim(string customValue) + { + public string CustomValue { get; set; } = customValue; + } + private class CustomPayloadClaim(string customValue) + { + public string CustomValue { get; set; } = customValue; } }