From bfb1fed204bcc0e26c2b9a60460d262e659ce533 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:37:04 -0700 Subject: [PATCH 01/24] Update JsonWebToken for child classes. --- .../Json/JsonWebToken.PayloadClaimSet.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index d98a916982..9bebbbce10 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -37,6 +37,23 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) { if (reader.TokenType == JsonTokenType.PropertyName) { + ReadPayloadValue(reader, claims); + } + // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. + else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) + break; + else if (!reader.Read()) + break; + }; + + return new JsonClaimSet(claims); + } + + /// + /// + /// + protected internal virtual void ReadPayloadValue(Utf8JsonReader reader, Dictionary claims) + { if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { _audiences = []; @@ -103,14 +120,5 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); } } - // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. - else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) - break; - else if (!reader.Read()) - break; - }; - - return new JsonClaimSet(claims); - } } } From 4eb7a5fcc45aaf2d0dd0b38f81639964b39a2cac Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:52:29 -0700 Subject: [PATCH 02/24] Add props files to solution. --- Wilson.sln | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Wilson.sln b/Wilson.sln index 888f40d85c..87f8c6e7fd 100644 --- a/Wilson.sln +++ b/Wilson.sln @@ -105,6 +105,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.Aot EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.Benchmarks", "benchmark\Microsoft.IdentityModel.Benchmarks\Microsoft.IdentityModel.Benchmarks.csproj", "{F1BB31E4-8865-4425-8BD4-94F1815C16E0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{A426FA3A-9323-4EEF-B312-CA5C266AEA03}" + ProjectSection(SolutionItems) = preProject + build\common.props = build\common.props + build\commonTest.props = build\commonTest.props + build\dependencies.props = build\dependencies.props + build\dependenciesTest.props = build\dependenciesTest.props + build\targets.props = build\targets.props + build\targetsTest.props = build\targetsTest.props + build\version.props = build\version.props + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 76d9e5246129a5853804c5e3296630823527ed55 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 27 Mar 2024 01:42:43 -0700 Subject: [PATCH 03/24] Fix. --- .../Json/JsonWebToken.PayloadClaimSet.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 9bebbbce10..83619c33e4 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -37,7 +37,7 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) { if (reader.TokenType == JsonTokenType.PropertyName) { - ReadPayloadValue(reader, claims); + ReadPayloadValue(ref reader, claims); } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -52,8 +52,10 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) /// /// /// - protected internal virtual void ReadPayloadValue(Utf8JsonReader reader, Dictionary claims) + protected internal virtual void ReadPayloadValue(ref Utf8JsonReader reader, Dictionary claims) { + _ = claims ?? throw new ArgumentNullException(nameof(claims)); + if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { _audiences = []; From b08ffa1e00bc63289135e5fe0560549b706d8232 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:32:02 -0700 Subject: [PATCH 04/24] Add another claims dictionary to JsonWebToken.Payload to hold string claims as UTF8 ReadOnlyMemory. --- .../Json/JsonClaimSet.cs | 52 ++++++- .../Json/JsonWebToken.PayloadClaimSet.cs | 129 +++++++++--------- 2 files changed, 112 insertions(+), 69 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 75aa609139..da8fa98899 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -7,6 +7,7 @@ using System.Collections.ObjectModel; using System.Globalization; using System.Security.Claims; +using System.Text; using System.Text.Json; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; @@ -24,15 +25,26 @@ internal class JsonClaimSet internal object _claimsLock = new(); internal readonly Dictionary _jsonClaims; + internal readonly Dictionary> _jsonClaimsUtf8; private List _claims; - internal JsonClaimSet() { _jsonClaims = new Dictionary(); } + internal JsonClaimSet() + { + _jsonClaims = new(); + _jsonClaimsUtf8 = new(); + } internal JsonClaimSet(Dictionary jsonClaims) { _jsonClaims = jsonClaims; } + internal JsonClaimSet(Dictionary jsonClaims, Dictionary> jsonClaimsUtf8) + { + _jsonClaims = jsonClaims; + _jsonClaimsUtf8 = jsonClaimsUtf8; + } + internal List Claims(string issuer) { if (_claims == null) @@ -71,7 +83,7 @@ internal static void CreateClaimFromObject(List claims, string claimType, else if (value is double d) claims.Add(new Claim(claimType, d.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Double, issuer, issuer)); else if (value is DateTime dt) - claims.Add(new Claim(claimType, dt.ToString("o",CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); + claims.Add(new Claim(claimType, dt.ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, issuer, issuer)); else if (value is float f) claims.Add(new Claim(claimType, f.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Double, issuer, issuer)); else if (value is decimal m) @@ -159,6 +171,12 @@ internal Claim GetClaim(string key, string issuer) internal string GetStringValue(string key) { +#if NET7_0_OR_GREATER + if (_jsonClaimsUtf8.TryGetValue(key, out ReadOnlyMemory stringBytes)) + { + return Encoding.UTF8.GetString(stringBytes.Span); + } +#else if (_jsonClaims.TryGetValue(key, out object obj)) { if (obj == null) @@ -166,10 +184,23 @@ internal string GetStringValue(string key) return obj.ToString(); } +#endif return string.Empty; } +#if NET7_0_OR_GREATER + internal ReadOnlySpan GetUtf8StringValue(string key) + { + if (_jsonClaimsUtf8.TryGetValue(key, out ReadOnlyMemory stringBytes)) + { + return stringBytes.Span; + } + + return new Span(); + } +#endif + internal DateTime GetDateTime(string key) { long l = GetValue(key, false, out bool found); @@ -317,7 +348,7 @@ internal T GetValue(string key, bool throwEx, out bool found) else if (typeof(T) == typeof(Collection)) return (T)(object)new Collection { obj }; - else if(typeof(T).IsEnum) + else if (typeof(T).IsEnum) { return (T)Enum.Parse(typeof(T), obj.ToString(), ignoreCase: true); } @@ -339,7 +370,7 @@ internal T GetValue(string key, bool throwEx, out bool found) if (objType == typeof(long)) return (T)(object)new long[] { (long)obj }; - if(objType == typeof(int)) + if (objType == typeof(int)) return (T)(object)new long[] { (int)obj }; if (long.TryParse(obj.ToString(), out long value)) @@ -347,7 +378,7 @@ internal T GetValue(string key, bool throwEx, out bool found) } else if (typeof(T) == typeof(double)) { - if(double.TryParse(obj.ToString(), out double value)) + if (double.TryParse(obj.ToString(), out double value)) return (T)(object)value; } else if (typeof(T) == typeof(uint)) @@ -422,6 +453,17 @@ internal bool TryGetClaim(string key, string issuer, out Claim claim) /// internal bool TryGetValue(string key, out T value) { +#if NET7_0_OR_GREATER + if (typeof(T) == typeof(string)) + { + var span = GetUtf8StringValue(key); + if (!span.IsEmpty) + { + value = (T)(object)Encoding.UTF8.GetString(span); + return true; + } + } +#endif value = GetValue(key, false, out bool found); return found; } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 83619c33e4..efdc98fe1b 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -18,7 +18,7 @@ internal JsonClaimSet CreatePayloadClaimSet(byte[] bytes, int length) } internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) - { + { Utf8JsonReader reader = new(byteSpan); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, true)) throw LogHelper.LogExceptionMessage( @@ -33,11 +33,12 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) LogHelper.MarkAsNonPII(reader.BytesConsumed)))); Dictionary claims = []; + Dictionary> claimsUtf8 = []; while (true) { if (reader.TokenType == JsonTokenType.PropertyName) { - ReadPayloadValue(ref reader, claims); + ReadPayloadValue(ref reader, claims, claimsUtf8); } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -46,81 +47,81 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) break; }; - return new JsonClaimSet(claims); + return new JsonClaimSet(claims, claimsUtf8); } /// /// /// - protected internal virtual void ReadPayloadValue(ref Utf8JsonReader reader, Dictionary claims) + protected internal virtual void ReadPayloadValue(ref Utf8JsonReader reader, Dictionary claims, Dictionary> claimsUtf8) { _ = claims ?? throw new ArgumentNullException(nameof(claims)); - if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) - { - _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; - } - } - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) - { - _azp = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); - claims[JwtRegisteredClaimNames.Azp] = _azp; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Exp)) - { - _exp = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true); - _expDateTime = EpochTime.DateTime(_exp.Value); - claims[JwtRegisteredClaimNames.Exp] = _exp; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iat)) - { - _iat = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true); - _iatDateTime = EpochTime.DateTime(_iat.Value); - claims[JwtRegisteredClaimNames.Iat] = _iat; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss)) - { - _iss = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); - claims[JwtRegisteredClaimNames.Iss] = _iss; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti)) - { - _jti = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); - claims[JwtRegisteredClaimNames.Jti] = _jti; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Nbf)) - { - _nbf = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true); - _nbfDateTime = EpochTime.DateTime(_nbf.Value); - claims[JwtRegisteredClaimNames.Nbf] = _nbf; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Sub)) + if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) + { + _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) { - _sub = JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true); - claims[JwtRegisteredClaimNames.Sub] = _sub; + _audiences.Add(JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Aud, ClassName)); + claims[JwtRegisteredClaimNames.Aud] = _audiences[0]; } else { - string propertyName = reader.GetString(); - claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); + claims[JwtRegisteredClaimNames.Aud] = _audiences; } } + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) + { + _azp = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); + claims[JwtRegisteredClaimNames.Azp] = _azp; + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Exp)) + { + _exp = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true); + _expDateTime = EpochTime.DateTime(_exp.Value); + claims[JwtRegisteredClaimNames.Exp] = _exp; + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iat)) + { + _iat = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true); + _iatDateTime = EpochTime.DateTime(_iat.Value); + claims[JwtRegisteredClaimNames.Iat] = _iat; + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss)) + { + _iss = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); + claims[JwtRegisteredClaimNames.Iss] = _iss; + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti)) + { + _jti = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); + claims[JwtRegisteredClaimNames.Jti] = _jti; + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Nbf)) + { + _nbf = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true); + _nbfDateTime = EpochTime.DateTime(_nbf.Value); + claims[JwtRegisteredClaimNames.Nbf] = _nbf; + } + 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); + } + } } } From 22720d7071ae3a6dd191770e4f2577411bf26c2f Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 3 Apr 2024 23:34:09 -0700 Subject: [PATCH 05/24] Using and reading from the original token as a Span and saving claim value indices instead of creating Memor for each claim. --- .../Json/JsonClaimSet.cs | 17 +++++++++++------ .../Json/JsonWebToken.PayloadClaimSet.cs | 18 +++++++++++------- .../JsonWebToken.cs | 2 +- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index da8fa98899..7d67a614ff 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -25,7 +25,8 @@ internal class JsonClaimSet internal object _claimsLock = new(); internal readonly Dictionary _jsonClaims; - internal readonly Dictionary> _jsonClaimsUtf8; + internal readonly Dictionary _jsonClaimsUtf8; + internal readonly Memory _tokenUtf8; private List _claims; internal JsonClaimSet() @@ -39,10 +40,14 @@ internal JsonClaimSet(Dictionary jsonClaims) _jsonClaims = jsonClaims; } - internal JsonClaimSet(Dictionary jsonClaims, Dictionary> jsonClaimsUtf8) + internal JsonClaimSet( + Dictionary jsonClaims, + Dictionary jsonClaimsUtf8, + Memory tokenUtf8) { _jsonClaims = jsonClaims; _jsonClaimsUtf8 = jsonClaimsUtf8; + _tokenUtf8 = tokenUtf8; } internal List Claims(string issuer) @@ -172,9 +177,9 @@ internal Claim GetClaim(string key, string issuer) internal string GetStringValue(string key) { #if NET7_0_OR_GREATER - if (_jsonClaimsUtf8.TryGetValue(key, out ReadOnlyMemory stringBytes)) + if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) { - return Encoding.UTF8.GetString(stringBytes.Span); + return Encoding.UTF8.GetString(_tokenUtf8.Slice(tuple.Item1, tuple.Item2).Span); } #else if (_jsonClaims.TryGetValue(key, out object obj)) @@ -192,9 +197,9 @@ internal string GetStringValue(string key) #if NET7_0_OR_GREATER internal ReadOnlySpan GetUtf8StringValue(string key) { - if (_jsonClaimsUtf8.TryGetValue(key, out ReadOnlyMemory stringBytes)) + if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) { - return stringBytes.Span; + return _tokenUtf8.Slice(tuple.Item1, tuple.Item2).Span; } return new Span(); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index efdc98fe1b..5f00bcb260 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -14,12 +14,12 @@ public partial class JsonWebToken { internal JsonClaimSet CreatePayloadClaimSet(byte[] bytes, int length) { - return CreatePayloadClaimSet(bytes.AsSpan(0, length)); + return CreatePayloadClaimSet(bytes.AsMemory(0, length)); } - internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) + internal JsonClaimSet CreatePayloadClaimSet(Memory byteSpan) { - Utf8JsonReader reader = new(byteSpan); + Utf8JsonReader reader = new(byteSpan.Span); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, true)) throw LogHelper.LogExceptionMessage( new JsonException( @@ -33,12 +33,12 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) LogHelper.MarkAsNonPII(reader.BytesConsumed)))); Dictionary claims = []; - Dictionary> claimsUtf8 = []; + Dictionary claimsUtf8 = []; while (true) { if (reader.TokenType == JsonTokenType.PropertyName) { - ReadPayloadValue(ref reader, claims, claimsUtf8); + ReadPayloadValue(ref reader, claims, claimsUtf8, byteSpan); } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -47,13 +47,17 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) break; }; - return new JsonClaimSet(claims, claimsUtf8); + return new JsonClaimSet(claims, claimsUtf8, byteSpan); } /// /// /// - protected internal virtual void ReadPayloadValue(ref Utf8JsonReader reader, Dictionary claims, Dictionary> claimsUtf8) + protected internal virtual void ReadPayloadValue( + ref Utf8JsonReader reader, + Dictionary claims, + Dictionary claimsUtf8, + Memory tokenUtf8) { _ = claims ?? throw new ArgumentNullException(nameof(claims)); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index b7b66a3596..9b79591123 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -573,7 +573,7 @@ internal JsonClaimSet CreateClaimSet(ReadOnlySpan strSpan, int startIndex, try { Base64UrlEncoding.Decode(strSpan, startIndex, length, output); - return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsSpan()); + return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsMemory()); } finally { From e5971604f428630f2d883cfc00ed54bb41d78a03 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:04:34 -0700 Subject: [PATCH 06/24] Rename. --- .../Json/JsonClaimSet.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 7d67a614ff..8ed460d340 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -195,7 +195,7 @@ internal string GetStringValue(string key) } #if NET7_0_OR_GREATER - internal ReadOnlySpan GetUtf8StringValue(string key) + internal ReadOnlySpan GetUtf8Bytes(string key) { if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) { @@ -461,7 +461,7 @@ internal bool TryGetValue(string key, out T value) #if NET7_0_OR_GREATER if (typeof(T) == typeof(string)) { - var span = GetUtf8StringValue(key); + var span = GetUtf8Bytes(key); if (!span.IsEmpty) { value = (T)(object)Encoding.UTF8.GetString(span); From 63ab5cd1c5e5c5419233339190ee1705029bdfb6 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:05:07 -0700 Subject: [PATCH 07/24] Remove ArrayPool from payload claim set. --- .../JsonWebToken.cs | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 6ba75cfed4..5ba0bc8982 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -226,7 +226,7 @@ public string EncodedHeader if (!_encodedTokenMemory.IsEmpty) _encodedHeader = _encodedTokenMemory.Span.Slice(0, Dot1).ToString(); else - _encodedHeader = (_encodedToken is not null) ? _encodedToken.Substring(0, Dot1) : string.Empty; + _encodedHeader = (_encodedToken is not null) ? _encodedToken.Substring(0, Dot1) : string.Empty; } return _encodedHeader; @@ -325,10 +325,10 @@ public string EncodedToken { get { - if (_encodedToken is null && !_encodedTokenMemory.IsEmpty) + if (_encodedToken is null && !_encodedTokenMemory.IsEmpty) _encodedToken = _encodedTokenMemory.ToString(); - return _encodedToken; + return _encodedToken; } } @@ -396,7 +396,7 @@ public string InitializationVector /// public override SecurityKey SigningKey { get; set; } - internal byte[] MessageBytes{ get; set; } + internal byte[] MessageBytes { get; set; } internal int NumberOfDots { get; set; } @@ -412,7 +412,7 @@ internal void ReadToken(ReadOnlyMemory encodedTokenMemory) // JWT must have 2 dots for JWS or 4 dots for JWE (a.b.c.d.e) ReadOnlySpan encodedTokenSpan = encodedTokenMemory.Span; - Dot1 = encodedTokenSpan.IndexOf('.'); + Dot1 = encodedTokenSpan.IndexOf('.'); if (Dot1 == -1 || Dot1 == encodedTokenSpan.Length - 1) throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14100)); @@ -466,11 +466,11 @@ internal void ReadToken(ReadOnlyMemory encodedTokenMemory) Dot3 = Dot2 + Dot3 + 1; if (Dot3 == encodedTokenSpan.Length - 1) throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14121)); - + Dot4 = encodedTokenSpan.Slice(Dot3 + 1).IndexOf('.'); if (Dot4 == -1) throw LogHelper.LogExceptionMessage(new SecurityTokenMalformedException(LogMessages.IDX14121)); - + Dot4 = Dot3 + Dot4 + 1; // must have something after 4th dot @@ -570,16 +570,9 @@ internal JsonClaimSet CreateClaimSet(ReadOnlySpan strSpan, int startIndex, { int outputSize = Base64UrlEncoding.ValidateAndGetOutputSize(strSpan, startIndex, length); - byte[] output = ArrayPool.Shared.Rent(outputSize); - try - { - Base64UrlEncoder.Decode(strSpan.Slice(startIndex, length), output); - return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsMemory()); - } - finally - { - ArrayPool.Shared.Return(output, true); - } + byte[] output = new byte[outputSize]; + Base64UrlEncoder.Decode(strSpan.Slice(startIndex, length), output); + return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsMemory()); } /// @@ -952,13 +945,13 @@ public string Zip /// If the 'actort' claim is not found, an empty string is returned. /// public string Actor + { + get { - get - { - _act ??= Payload.GetStringValue(JwtRegisteredClaimNames.Actort); - return _act; - } + _act ??= Payload.GetStringValue(JwtRegisteredClaimNames.Actort); + return _act; } + } /// /// Gets the list of 'aud' claims from the payload. From bc3140cef432aca8284a104d7af0f41fe6266166 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:10:20 -0700 Subject: [PATCH 08/24] Rename. --- .../Json/JsonClaimSet.cs | 10 +++++----- .../Json/JsonWebToken.PayloadClaimSet.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 8ed460d340..6f8ff8ceeb 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -26,7 +26,7 @@ internal class JsonClaimSet internal object _claimsLock = new(); internal readonly Dictionary _jsonClaims; internal readonly Dictionary _jsonClaimsUtf8; - internal readonly Memory _tokenUtf8; + internal readonly Memory _tokenAsMemory; private List _claims; internal JsonClaimSet() @@ -43,11 +43,11 @@ internal JsonClaimSet(Dictionary jsonClaims) internal JsonClaimSet( Dictionary jsonClaims, Dictionary jsonClaimsUtf8, - Memory tokenUtf8) + Memory tokenAsMemory) { _jsonClaims = jsonClaims; _jsonClaimsUtf8 = jsonClaimsUtf8; - _tokenUtf8 = tokenUtf8; + _tokenAsMemory = tokenAsMemory; } internal List Claims(string issuer) @@ -179,7 +179,7 @@ internal string GetStringValue(string key) #if NET7_0_OR_GREATER if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) { - return Encoding.UTF8.GetString(_tokenUtf8.Slice(tuple.Item1, tuple.Item2).Span); + return Encoding.UTF8.GetString(_tokenAsMemory.Slice(tuple.Item1, tuple.Item2).Span); } #else if (_jsonClaims.TryGetValue(key, out object obj)) @@ -199,7 +199,7 @@ internal ReadOnlySpan GetUtf8Bytes(string key) { if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) { - return _tokenUtf8.Slice(tuple.Item1, tuple.Item2).Span; + return _tokenAsMemory.Slice(tuple.Item1, tuple.Item2).Span; } return new Span(); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 5f00bcb260..720bab1707 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -17,9 +17,9 @@ internal JsonClaimSet CreatePayloadClaimSet(byte[] bytes, int length) return CreatePayloadClaimSet(bytes.AsMemory(0, length)); } - internal JsonClaimSet CreatePayloadClaimSet(Memory byteSpan) + internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) { - Utf8JsonReader reader = new(byteSpan.Span); + Utf8JsonReader reader = new(tokenPayloadAsMemory.Span); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, true)) throw LogHelper.LogExceptionMessage( new JsonException( @@ -38,7 +38,7 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory byteSpan) { if (reader.TokenType == JsonTokenType.PropertyName) { - ReadPayloadValue(ref reader, claims, claimsUtf8, byteSpan); + ReadPayloadValue(ref reader, claims, claimsUtf8, tokenPayloadAsMemory); } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -47,7 +47,7 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory byteSpan) break; }; - return new JsonClaimSet(claims, claimsUtf8, byteSpan); + return new JsonClaimSet(claims, claimsUtf8, tokenPayloadAsMemory); } /// @@ -57,7 +57,7 @@ protected internal virtual void ReadPayloadValue( ref Utf8JsonReader reader, Dictionary claims, Dictionary claimsUtf8, - Memory tokenUtf8) + Memory tokenAsMemory) { _ = claims ?? throw new ArgumentNullException(nameof(claims)); From 8397e3416b72a5ae394796b03e57b69cae39a179 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:30:28 -0700 Subject: [PATCH 09/24] Add ReadUtf8StringLocation --- .../Json/JsonClaimSet.cs | 1 + .../Json/JsonSerializerPrimitives.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 6f8ff8ceeb..855f1cc0d2 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -197,6 +197,7 @@ internal string GetStringValue(string key) #if NET7_0_OR_GREATER internal ReadOnlySpan GetUtf8Bytes(string key) { + // (int startIndex, int length) tuple if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) { return _tokenAsMemory.Slice(tuple.Item1, tuple.Item2).Span; diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index cd99a503ca..d0242482e7 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -644,6 +644,19 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName return retval; } + internal static (int ValueStartIndex, int ValueLength)? ReadUtf8StringLocation(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)); + + return ((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length); + } + 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. From 2980f4188d1afc82d8993071c470fd643dd9f958 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Fri, 3 May 2024 18:14:32 -0700 Subject: [PATCH 10/24] Make private protected. --- .../Json/JsonWebToken.PayloadClaimSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 720bab1707..5c00ce2aa1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -53,7 +53,7 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) /// /// /// - protected internal virtual void ReadPayloadValue( + private protected virtual void ReadPayloadValue( ref Utf8JsonReader reader, Dictionary claims, Dictionary claimsUtf8, From fb3639d51bd5b40be248b0394404010077c5a37d Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Tue, 18 Jun 2024 00:31:54 -0700 Subject: [PATCH 11/24] Refactor. --- .../Json/JsonClaimSet.cs | 42 +++++--- .../Json/JsonWebToken.PayloadClaimSet.cs | 99 ++++++++++++++++--- .../JsonWebToken.cs | 60 +++++++++++ .../Json/JsonSerializerPrimitives.cs | 44 ++++++--- 4 files changed, 205 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 86a6949614..f1f4560973 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -25,31 +25,35 @@ internal class JsonClaimSet internal object _claimsLock = new(); internal readonly Dictionary _jsonClaims; - internal readonly Dictionary _jsonClaimsUtf8; +#if NET8_0_OR_GREATER + internal readonly Dictionary _jsonClaimsBytes; internal readonly Memory _tokenAsMemory; +#endif private List _claims; internal JsonClaimSet() { _jsonClaims = new(); - _jsonClaimsUtf8 = new(); +#if NET8_0_OR_GREATER + _jsonClaimsBytes = new(); +#endif } internal JsonClaimSet(Dictionary jsonClaims) { _jsonClaims = jsonClaims; } - +#if NET8_0_OR_GREATER internal JsonClaimSet( Dictionary jsonClaims, - Dictionary jsonClaimsUtf8, + Dictionary jsonClaimsBytes, Memory tokenAsMemory) { _jsonClaims = jsonClaims; - _jsonClaimsUtf8 = jsonClaimsUtf8; + _jsonClaimsBytes = jsonClaimsBytes; _tokenAsMemory = tokenAsMemory; } - +#endif internal List Claims(string issuer) { if (_claims == null) @@ -176,10 +180,13 @@ internal Claim GetClaim(string key, string issuer) internal string GetStringValue(string key) { -#if NET7_0_OR_GREATER - if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) +#if NET8_0_OR_GREATER + if (_jsonClaimsBytes.TryGetValue(key, out (int StartIndex, int Length)? location)) { - return Encoding.UTF8.GetString(_tokenAsMemory.Slice(tuple.Item1, tuple.Item2).Span); + if (!location.HasValue) + return null; + + return Encoding.UTF8.GetString(_tokenAsMemory.Slice(location.Value.StartIndex, location.Value.Length).Span); } #else if (_jsonClaims.TryGetValue(key, out object obj)) @@ -194,13 +201,16 @@ internal string GetStringValue(string key) return string.Empty; } -#if NET7_0_OR_GREATER - internal ReadOnlySpan GetUtf8Bytes(string key) +#if NET8_0_OR_GREATER + // Similar to GetStringValue but returns the bytes directly. + internal ReadOnlySpan GetStringBytesValue(string key) { - // (int startIndex, int length) tuple - if (_jsonClaimsUtf8.TryGetValue(key, out (int, int) tuple)) + if (_jsonClaimsBytes.TryGetValue(key, out (int StartIndex, int Length)? location)) { - return _tokenAsMemory.Slice(tuple.Item1, tuple.Item2).Span; + if (!location.HasValue) + return null; + + return _tokenAsMemory.Slice(location.Value.StartIndex, location.Value.Length).Span; } return new Span(); @@ -459,10 +469,10 @@ internal bool TryGetClaim(string key, string issuer, out Claim claim) /// internal bool TryGetValue(string key, out T value) { -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER if (typeof(T) == typeof(string)) { - var span = GetUtf8Bytes(key); + var span = GetStringBytesValue(key); if (!span.IsEmpty) { value = (T)(object)Encoding.UTF8.GetString(span); diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 5c00ce2aa1..20f9fe8dd4 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -33,12 +33,18 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) LogHelper.MarkAsNonPII(reader.BytesConsumed)))); Dictionary claims = []; - Dictionary claimsUtf8 = []; +#if NET8_0_OR_GREATER + Dictionary claimsBytes = []; +#endif while (true) { if (reader.TokenType == JsonTokenType.PropertyName) { - ReadPayloadValue(ref reader, claims, claimsUtf8, tokenPayloadAsMemory); +#if NET8_0_OR_GREATER + ReadPayloadValue(ref reader, claims, claimsBytes, tokenPayloadAsMemory); +#else + ReadPayloadValue(ref reader, claims); +#endif } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -47,20 +53,15 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) break; }; - return new JsonClaimSet(claims, claimsUtf8, tokenPayloadAsMemory); +#if NET8_0_OR_GREATER + return new JsonClaimSet(claims, claimsBytes, tokenPayloadAsMemory); +#else + return new JsonClaimSet(claims); +#endif } - /// - /// - /// - private protected virtual void ReadPayloadValue( - ref Utf8JsonReader reader, - Dictionary claims, - Dictionary claimsUtf8, - Memory tokenAsMemory) + private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) { - _ = claims ?? throw new ArgumentNullException(nameof(claims)); - if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { _audiences = []; @@ -127,5 +128,77 @@ private protected virtual void ReadPayloadValue( claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); } } + +#if NET8_0_OR_GREATER + private protected virtual void ReadPayloadValue( + ref Utf8JsonReader reader, + Dictionary claims, + Dictionary claimsBytes, + Memory tokenAsMemory) + { + if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) + { + _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; + } + } + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) + { + claimsBytes[JwtRegisteredClaimNames.Azp] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Azp, ClassName, true); + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Exp)) + { + _exp = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true); + _expDateTime = EpochTime.DateTime(_exp.Value); + claims[JwtRegisteredClaimNames.Exp] = _exp; + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iat)) + { + _iat = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true); + _iatDateTime = EpochTime.DateTime(_iat.Value); + claims[JwtRegisteredClaimNames.Iat] = _iat; + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss)) + { + claimsBytes[JwtRegisteredClaimNames.Iss] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Iss, ClassName, true); + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti)) + { + claimsBytes[JwtRegisteredClaimNames.Jti] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Jti, ClassName, true); + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Nbf)) + { + _nbf = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true); + _nbfDateTime = EpochTime.DateTime(_nbf.Value); + claims[JwtRegisteredClaimNames.Nbf] = _nbf; + } + 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); + } + } +#endif } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 5ba0bc8982..0c9c5291fc 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -1128,5 +1128,65 @@ internal DateTime? ValidToNullable } } #endregion + + #region Payload Properties Bytes +#if NET8_0_OR_GREATER + + /// + /// Gets the 'azp' claim from the payload. + /// + /// + /// Identifies the authorized party for the id_token. + /// see: https://openid.net/specs/openid-connect-core-1_0.html + /// + /// If the 'azp' claim is not found, an empty string is returned. + /// + /// + public ReadOnlySpan AzpBytes + { + get + { + return Payload.GetStringBytesValue(JwtRegisteredClaimNames.Azp); + } + } + + /// + /// Gets the 'value' of the 'jti' claim from the payload. + /// + /// + /// Provides a unique identifier for the JWT. + /// see: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7 + /// + /// If the 'jti' claim is not found, an empty string is returned. + /// + /// + public ReadOnlySpan IdBytes + { + get + { + return Payload.GetStringBytesValue(JwtRegisteredClaimNames.Jti); + } + } + + /// + /// Gets the 'value' of the 'iss' claim from the payload. + /// + /// + /// Identifies the principal that issued the JWT. + /// see: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 + /// + /// If the 'iss' claim is not found, an empty string is returned. + /// + /// + public ReadOnlySpan IssuerBytes + { + get + { + return Payload.GetStringBytesValue(JwtRegisteredClaimNames.Iss); + } + } + +#endif + #endregion } } diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index 3c119eb1dd..2aa3e77f7b 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -655,7 +655,14 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName return retval; } - internal static (int ValueStartIndex, int ValueLength)? ReadUtf8StringLocation(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) +#if NET8_0_OR_GREATER + // Mostly the same as ReadString, but this method returns the location of the string in the token. + internal static (int StartIndex, int Length)? ReadStringBytesLocation( + ref Utf8JsonReader reader, + Memory tokenAsMemory, + string propertyName, + string className, + bool read = false) { // returning null keeps the same logic as JsonSerialization.ReadObject if (IsReaderPositionedOnNull(ref reader, read, true)) @@ -665,8 +672,23 @@ internal static (int ValueStartIndex, int ValueLength)? ReadUtf8StringLocation(r throw LogHelper.LogExceptionMessage( CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); - return ((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length); + if (reader.ValueIsEscaped) + { + // Escaped string is always longer than unescaped + // Unescapes in-place + int bytesWritten = reader.CopyString(tokenAsMemory.Span.Slice((int)reader.TokenStartIndex, reader.ValueSpan.Length)); + + return ((int)reader.TokenStartIndex, bytesWritten); + } + + var location = ((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length); + + // Move to next token + reader.Read(); + + return location; } +#endif internal static string ReadStringAsBool(ref Utf8JsonReader reader, string propertyName, string className, bool read = false) { @@ -1078,7 +1100,7 @@ private static bool IsReaderPositionedOnNull(ref Utf8JsonReader reader, bool rea return true; } - #endregion +#endregion #region Write public static void WriteAsJsonElement(ref Utf8JsonWriter writer, string json) @@ -1165,7 +1187,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj #if NET6_0_OR_GREATER writer.WriteNumber(key, dub); #else - #pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. +#pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. try { if (decimal.TryParse(dub.ToString(CultureInfo.InvariantCulture), out decimal dec)) @@ -1177,7 +1199,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj { writer.WriteNumber(key, dub); } - #pragma warning restore CA1031 +#pragma warning restore CA1031 #endif else if (obj is decimal d) writer.WriteNumber(key, d); @@ -1187,7 +1209,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj #if NET6_0_OR_GREATER writer.WriteNumber(key, f); #else - #pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. +#pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. try { if (decimal.TryParse(f.ToString(CultureInfo.InvariantCulture), out decimal dec)) @@ -1199,7 +1221,7 @@ public static void WriteObject(ref Utf8JsonWriter writer, string key, object obj { writer.WriteNumber(key, f); } - #pragma warning restore CA1031 +#pragma warning restore CA1031 #endif else if (obj is Guid g) writer.WriteString(key, g); @@ -1253,7 +1275,7 @@ public static void WriteObjectValue(ref Utf8JsonWriter writer, object obj) #if NET6_0_OR_GREATER writer.WriteNumberValue(dub); #else - #pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. +#pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. try { if (decimal.TryParse(dub.ToString(CultureInfo.InvariantCulture), out decimal dec)) @@ -1265,7 +1287,7 @@ public static void WriteObjectValue(ref Utf8JsonWriter writer, object obj) { writer.WriteNumberValue(dub); } - #pragma warning restore CA1031 +#pragma warning restore CA1031 #endif else if (obj is JsonElement j) j.WriteTo(writer); @@ -1295,7 +1317,7 @@ public static void WriteObjectValue(ref Utf8JsonWriter writer, object obj) #if NET6_0_OR_GREATER writer.WriteNumberValue(f); #else - #pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. +#pragma warning disable CA1031 // Do not catch general exception types, we have seen TryParse fault. try { if (decimal.TryParse(f.ToString(CultureInfo.InvariantCulture), out decimal dec)) @@ -1307,7 +1329,7 @@ public static void WriteObjectValue(ref Utf8JsonWriter writer, object obj) { writer.WriteNumberValue(f); } - #pragma warning restore CA1031 +#pragma warning restore CA1031 #endif else From 7e4f21e54195abbb6394704641c6b452f74770ab Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Tue, 18 Jun 2024 00:32:16 -0700 Subject: [PATCH 12/24] Add test. --- .../JsonWebTokenTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 08644135d1..a8e1ba983e 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Reflection; using System.Security.Claims; +using System.Security.Cryptography; using System.Text; using System.Threading; using Microsoft.IdentityModel.TestUtils; @@ -1514,6 +1515,56 @@ public static TheoryData ParseTimeValuesTheoryData } } +#if NET8_0_OR_GREATER + [Fact] + public void ParseToken_WithByteProperties() + { + // Arrange + var escapedAzp = "azp\u0027azp"; + var unescapedAzp = "azp'azp"; + + RSA rsa = RSA.Create(2048); + RSAParameters rsaParameters = rsa.ExportParameters(true); + RsaSecurityKey rsaSecurityKey = new(rsaParameters) { KeyId = "RsaPrivate" }; + + var jwsTokenDescriptor = new SecurityTokenDescriptor + { + SigningCredentials = new(rsaSecurityKey, SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Sha256), + 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, escapedAzp }, + { JwtRegisteredClaimNames.Jti, Default.Jti }, + { "uknown_claim", "unknown_claim_value" }, + } + }; + + var tokenStr = new JsonWebTokenHandler().CreateToken(jwsTokenDescriptor); + + // Act + var jwt = new JsonWebToken(tokenStr); + jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Iss, out string issuerFromPayload); + jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Jti, out string jtiFromPayload); + jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Azp, out string azpFromPayload); + + // Assert + Assert.Equal(Default.Issuer, issuerFromPayload); + Assert.Equal(Default.Issuer, jwt.Issuer); + Assert.True(jwt.IssuerBytes.SequenceEqual(Encoding.UTF8.GetBytes(Default.Issuer))); + Assert.Equal(Default.Jti, jtiFromPayload); + Assert.Equal(Default.Jti, jwt.Id); + Assert.True(jwt.IdBytes.SequenceEqual(Encoding.UTF8.GetBytes(Default.Jti))); + Assert.Equal(unescapedAzp, azpFromPayload); + Assert.Equal(unescapedAzp, jwt.Azp); + Assert.True(jwt.AzpBytes.SequenceEqual(Encoding.UTF8.GetBytes(unescapedAzp))); + } +#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. From 7b968bed4e430ce6253205419b5dd88deac3cf62 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 19 Jun 2024 12:39:17 -0700 Subject: [PATCH 13/24] Fixes to claims. --- .../Json/JsonClaimSet.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index f1f4560973..ba666daf72 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -25,24 +25,33 @@ internal class JsonClaimSet internal object _claimsLock = new(); internal readonly Dictionary _jsonClaims; + #if NET8_0_OR_GREATER internal readonly Dictionary _jsonClaimsBytes; internal readonly Memory _tokenAsMemory; #endif + private List _claims; internal JsonClaimSet() { - _jsonClaims = new(); + _jsonClaims = []; + #if NET8_0_OR_GREATER - _jsonClaimsBytes = new(); + _jsonClaimsBytes = []; + _tokenAsMemory = Memory.Empty; #endif } internal JsonClaimSet(Dictionary jsonClaims) { _jsonClaims = jsonClaims; + +#if NET8_0_OR_GREATER + _jsonClaimsBytes = []; +#endif } + #if NET8_0_OR_GREATER internal JsonClaimSet( Dictionary jsonClaims, @@ -54,6 +63,7 @@ internal JsonClaimSet( _tokenAsMemory = tokenAsMemory; } #endif + internal List Claims(string issuer) { if (_claims == null) @@ -69,6 +79,14 @@ internal List CreateClaims(string issuer) foreach (KeyValuePair kvp in _jsonClaims) CreateClaimFromObject(claims, kvp.Key, kvp.Value, issuer); +#if NET8_0_OR_GREATER + // _jsonClaimsBytes is only for string values for known claims, which would not be in _jsonClaims. + foreach (KeyValuePair kvp in _jsonClaimsBytes) + { + string value = Encoding.UTF8.GetString(_tokenAsMemory.Slice(kvp.Value.Value.StartIndex, kvp.Value.Value.Length).Span); + claims.Add(new Claim(kvp.Key, value, ClaimValueTypes.String, issuer, issuer)); + } +#endif return claims; } @@ -168,7 +186,11 @@ internal Claim GetClaim(string key, string issuer) if (key == null) throw LogHelper.LogArgumentNullException(nameof(key)); +#if NET8_0_OR_GREATER + if (_jsonClaims.TryGetValue(key, out object _) || _jsonClaimsBytes.TryGetValue(key, out _)) +#else if (_jsonClaims.TryGetValue(key, out object _)) +#endif { foreach (var claim in Claims(issuer)) if (claim.Type == key) @@ -197,7 +219,6 @@ internal string GetStringValue(string key) return obj.ToString(); } #endif - return string.Empty; } @@ -486,7 +507,11 @@ internal bool TryGetValue(string key, out T value) internal bool HasClaim(string claimName) { - return _jsonClaims.TryGetValue(claimName, out _); +#if NET8_0_OR_GREATER + return _jsonClaims.ContainsKey(claimName) || _jsonClaimsBytes.ContainsKey(claimName); +#else + return _jsonClaims.ContainsKey(claimName); +#endif } } } From aac0539b6a16354547ab3c8385930ba503db67b4 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 19 Jun 2024 23:37:09 -0700 Subject: [PATCH 14/24] Update reading string header properties. --- .../Json/JsonWebToken.HeaderClaimSet.cs | 142 +++++++++++++----- .../Json/JsonWebToken.PayloadClaimSet.cs | 5 + .../JsonWebToken.cs | 89 +++++------ .../JsonWebTokenTests.cs | 2 +- 4 files changed, 141 insertions(+), 97 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs index 1c8bcc20aa..0f40a709ba 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs @@ -13,17 +13,17 @@ public partial class JsonWebToken { internal JsonClaimSet CreateHeaderClaimSet(byte[] bytes) { - return CreateHeaderClaimSet(bytes.AsSpan()); + return CreateHeaderClaimSet(bytes.AsMemory()); } internal JsonClaimSet CreateHeaderClaimSet(byte[] bytes, int length) { - return CreateHeaderClaimSet(bytes.AsSpan(0, length)); + return CreateHeaderClaimSet(bytes.AsMemory(0, length)); } - internal JsonClaimSet CreateHeaderClaimSet(ReadOnlySpan byteSpan) - { - Utf8JsonReader reader = new(byteSpan); + internal JsonClaimSet CreateHeaderClaimSet(Memory tokenHeaderAsMemory) + { + Utf8JsonReader reader = new(tokenHeaderAsMemory.Span); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, true)) throw LogHelper.LogExceptionMessage( new JsonException( @@ -36,46 +36,19 @@ internal JsonClaimSet CreateHeaderClaimSet(ReadOnlySpan byteSpan) LogHelper.MarkAsNonPII(reader.CurrentDepth), LogHelper.MarkAsNonPII(reader.BytesConsumed)))); - Dictionary claims = new(); + Dictionary claims = []; +#if NET8_0_OR_GREATER + Dictionary claimsBytes = []; +#endif 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); - } +#if NET8_0_OR_GREATER + ReadHeaderValue(ref reader, claims, claimsBytes, tokenHeaderAsMemory); +#else + ReadHeaderValue(ref reader, claims); +#endif } // We read a JsonTokenType.StartObject above, exiting and positioning reader at next token. else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false)) @@ -84,7 +57,94 @@ internal JsonClaimSet CreateHeaderClaimSet(ReadOnlySpan byteSpan) break; }; +#if NET8_0_OR_GREATER + return new JsonClaimSet(claims, claimsBytes, tokenHeaderAsMemory); +#else return new JsonClaimSet(claims); +#endif + } + + private protected virtual void ReadHeaderValue(ref Utf8JsonReader reader, IDictionary claims) + { + _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); + + 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); + } + } + +#if NET8_0_OR_GREATER + private protected virtual void ReadHeaderValue( + ref Utf8JsonReader reader, + Dictionary claims, + Dictionary claimsBytes, + Memory tokenAsMemory) + { + _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); + _ = claimsBytes ?? throw LogHelper.LogArgumentNullException(nameof(claimsBytes)); + + if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) + { + claimsBytes[JwtHeaderParameterNames.Alg] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Alg, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Cty)) + { + claimsBytes[JwtHeaderParameterNames.Cty] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Cty, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Kid)) + { + claimsBytes[JwtHeaderParameterNames.Kid] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Kid, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Typ)) + { + claimsBytes[JwtHeaderParameterNames.Typ] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Typ, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.X5t)) + { + claimsBytes[JwtHeaderParameterNames.X5t] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.X5t, ClassName, true); + } + else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Zip)) + { + claimsBytes[JwtHeaderParameterNames.Zip] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Zip, ClassName, true); + } + else + { + string propertyName = reader.GetString(); + claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); + } } +#endif } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 20f9fe8dd4..87d594b7c2 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -62,6 +62,8 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) { + _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); + if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { _audiences = []; @@ -136,6 +138,9 @@ private protected virtual void ReadPayloadValue( Dictionary claimsBytes, Memory tokenAsMemory) { + _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); + _ = claimsBytes ?? throw LogHelper.LogArgumentNullException(nameof(claimsBytes)); + if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { _audiences = []; diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 0c9c5291fc..daecc01458 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -503,7 +503,7 @@ internal void ReadToken(ReadOnlyMemory encodedTokenMemory) try { - Header = CreateHeaderClaimSet(Base64UrlEncoder.Decode(headerSpan).AsSpan()); + Header = CreateHeaderClaimSet(Base64UrlEncoder.Decode(headerSpan).AsMemory()); } catch (Exception ex) { @@ -572,7 +572,7 @@ internal JsonClaimSet CreateClaimSet(ReadOnlySpan strSpan, int startIndex, byte[] output = new byte[outputSize]; Base64UrlEncoder.Decode(strSpan.Slice(startIndex, length), output); - return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsMemory()); + return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsMemory()) : CreatePayloadClaimSet(output.AsMemory()); } /// @@ -1129,64 +1129,43 @@ internal DateTime? ValidToNullable } #endregion - #region Payload Properties Bytes #if NET8_0_OR_GREATER - /// - /// Gets the 'azp' claim from the payload. - /// - /// - /// Identifies the authorized party for the id_token. - /// see: https://openid.net/specs/openid-connect-core-1_0.html - /// - /// If the 'azp' claim is not found, an empty string is returned. - /// - /// - public ReadOnlySpan AzpBytes - { - get - { - return Payload.GetStringBytesValue(JwtRegisteredClaimNames.Azp); - } - } + #region Header Properties Bytes - /// - /// Gets the 'value' of the 'jti' claim from the payload. - /// - /// - /// Provides a unique identifier for the JWT. - /// see: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7 - /// - /// If the 'jti' claim is not found, an empty string is returned. - /// - /// - public ReadOnlySpan IdBytes - { - get - { - return Payload.GetStringBytesValue(JwtRegisteredClaimNames.Jti); - } - } + /// + public ReadOnlySpan AlgBytes => Payload.GetStringBytesValue(JwtHeaderParameterNames.Alg); - /// - /// Gets the 'value' of the 'iss' claim from the payload. - /// - /// - /// Identifies the principal that issued the JWT. - /// see: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 - /// - /// If the 'iss' claim is not found, an empty string is returned. - /// - /// - public ReadOnlySpan IssuerBytes - { - get - { - return Payload.GetStringBytesValue(JwtRegisteredClaimNames.Iss); - } - } + /// + 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); -#endif #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/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index a8e1ba983e..d3f6dd5801 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -1540,7 +1540,7 @@ public void ParseToken_WithByteProperties() { JwtRegisteredClaimNames.Aud, Default.Audience }, { JwtRegisteredClaimNames.Azp, escapedAzp }, { JwtRegisteredClaimNames.Jti, Default.Jti }, - { "uknown_claim", "unknown_claim_value" }, + { "unknown_claim", "unknown_claim_value" }, } }; From 652b33361fb15dbb770e0c64a7193d6c25028ef4 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:57:13 -0700 Subject: [PATCH 15/24] Add delegate for reading properties instead of an overload. --- .../Json/JsonWebToken.PayloadClaimSet.cs | 38 +++++++++++-------- .../JsonWebToken.cs | 15 ++++++++ .../Delegates.cs | 9 +++++ .../TokenValidationParameters.cs | 5 +++ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 87d594b7c2..77a817e29c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -33,18 +33,13 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) LogHelper.MarkAsNonPII(reader.BytesConsumed)))); Dictionary claims = []; -#if NET8_0_OR_GREATER - Dictionary claimsBytes = []; -#endif + while (true) { if (reader.TokenType == JsonTokenType.PropertyName) { -#if NET8_0_OR_GREATER - ReadPayloadValue(ref reader, claims, claimsBytes, tokenPayloadAsMemory); -#else - ReadPayloadValue(ref reader, claims); -#endif + string claimName = reader.GetString(); + claims[claimName] = ReadTokenPayloadValue(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)) @@ -53,17 +48,30 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) break; }; -#if NET8_0_OR_GREATER - return new JsonClaimSet(claims, claimsBytes, tokenPayloadAsMemory); -#else return new JsonClaimSet(claims); -#endif } - private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) + /* + * Custom implemenation + ReadPayloadValueDelegate(ref Utf8JsonReader reader, string claimName) => + { + if (claimName == JwtRegisteredClaimNames.CustomProp) + { + return JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true); + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.CustomProp)) + { + return JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true); + } else + { + return JsonWebToken.ReadPayloadValue(ref reader, claimName); + } + } + */ + // Default implementation + // Read the value of the claimName from the reader into the dictionary. + public static object ReadPayloadValue(ref Utf8JsonReader reader, string claimName) { - _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); - if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { _audiences = []; diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index daecc01458..0b8dfabec8 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -86,6 +86,16 @@ public JsonWebToken(string jwtEncodedString) _encodedToken = jwtEncodedString; } + /// + /// + /// + /// + /// + public JsonWebToken(ReadOnlyMemory encodedTokenMemory, ReadTokenPayloadValue readTokenPayloadValueDelegate) : this(encodedTokenMemory) + { + ReadTokenPayloadValue = readTokenPayloadValueDelegate; + } + /// /// Initializes a new instance of from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format. /// @@ -142,6 +152,11 @@ public JsonWebToken(string header, string payload) _encodedToken = encodedToken; } + /// + /// + /// + internal ReadTokenPayloadValue ReadTokenPayloadValue { get; set; } = ReadPayloadValue; + internal string ActualIssuer { get; set; } internal ClaimsIdentity ActorClaimsIdentity { get; set; } diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index e2eaebb16a..479d198116 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 @@ -176,4 +177,12 @@ namespace Microsoft.IdentityModel.Tokens /// required for validation. /// A transformed . public delegate SecurityToken TransformBeforeSignatureValidation(SecurityToken token, TokenValidationParameters validationParameters); + + /// + /// Definition for ReadTokenPayloadValue. + /// + /// Reader for the underlying token bytes. + /// The name of the claim being read. + /// + public delegate object ReadTokenPayloadValue(ref Utf8JsonReader reader, string claimName); } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 2112772699..6d1a3bfedb 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -446,6 +446,11 @@ public string NameClaimType /// public IDictionary PropertyBag { get; set; } + /// + /// Gets or sets a delegate that will be called when reading token payload claims. + /// + public ReadTokenPayloadValue ReadTokenPayloadValue { get; set; } + /// /// Gets or sets a boolean to control if configuration required to be refreshed before token validation. /// From a7f36fbb53873b977ef5801c63023746b561543c Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Sat, 3 Aug 2024 00:32:53 -0700 Subject: [PATCH 16/24] Add delegate for reading properties instead of an overload. --- .../Json/JsonClaimSet.cs | 52 ++----- .../Json/JsonWebToken.HeaderClaimSet.cs | 80 ++++------ .../Json/JsonWebToken.PayloadClaimSet.cs | 140 +++++------------- .../JsonWebToken.cs | 19 ++- .../Delegates.cs | 14 +- .../Json/ClaimPosition.cs | 29 ++++ .../Json/JsonSerializerPrimitives.cs | 18 +-- .../TokenValidationParameters.cs | 7 +- .../CustomJsonWebToken.cs | 42 ------ .../JsonWebTokenTests.cs | 2 +- 10 files changed, 144 insertions(+), 259 deletions(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Json/ClaimPosition.cs delete mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index ba666daf72..1030ef99e3 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -27,7 +27,6 @@ internal class JsonClaimSet internal readonly Dictionary _jsonClaims; #if NET8_0_OR_GREATER - internal readonly Dictionary _jsonClaimsBytes; internal readonly Memory _tokenAsMemory; #endif @@ -38,7 +37,6 @@ internal JsonClaimSet() _jsonClaims = []; #if NET8_0_OR_GREATER - _jsonClaimsBytes = []; _tokenAsMemory = Memory.Empty; #endif } @@ -46,20 +44,14 @@ internal JsonClaimSet() internal JsonClaimSet(Dictionary jsonClaims) { _jsonClaims = jsonClaims; - -#if NET8_0_OR_GREATER - _jsonClaimsBytes = []; -#endif } #if NET8_0_OR_GREATER internal JsonClaimSet( Dictionary jsonClaims, - Dictionary jsonClaimsBytes, Memory tokenAsMemory) { _jsonClaims = jsonClaims; - _jsonClaimsBytes = jsonClaimsBytes; _tokenAsMemory = tokenAsMemory; } #endif @@ -77,16 +69,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 - // _jsonClaimsBytes is only for string values for known claims, which would not be in _jsonClaims. - foreach (KeyValuePair kvp in _jsonClaimsBytes) - { - string value = Encoding.UTF8.GetString(_tokenAsMemory.Slice(kvp.Value.Value.StartIndex, kvp.Value.Value.Length).Span); - claims.Add(new Claim(kvp.Key, value, ClaimValueTypes.String, issuer, issuer)); - } + if (kvp.Value is ClaimPosition position) + { + string value = Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); + claims.Add(new Claim(kvp.Key, value, ClaimValueTypes.String, issuer, issuer)); + } #endif + } return claims; } @@ -186,11 +179,7 @@ internal Claim GetClaim(string key, string issuer) if (key == null) throw LogHelper.LogArgumentNullException(nameof(key)); -#if NET8_0_OR_GREATER - if (_jsonClaims.TryGetValue(key, out object _) || _jsonClaimsBytes.TryGetValue(key, out _)) -#else if (_jsonClaims.TryGetValue(key, out object _)) -#endif { foreach (var claim in Claims(issuer)) if (claim.Type == key) @@ -202,23 +191,17 @@ internal Claim GetClaim(string key, string issuer) internal string GetStringValue(string key) { -#if NET8_0_OR_GREATER - if (_jsonClaimsBytes.TryGetValue(key, out (int StartIndex, int Length)? location)) - { - if (!location.HasValue) - return null; - - return Encoding.UTF8.GetString(_tokenAsMemory.Slice(location.Value.StartIndex, location.Value.Length).Span); - } -#else if (_jsonClaims.TryGetValue(key, out object obj)) { if (obj == null) return null; +#if NET8_0_OR_GREATER + if (obj is ClaimPosition position) + return Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); +#endif return obj.ToString(); } -#endif return string.Empty; } @@ -226,15 +209,16 @@ internal string GetStringValue(string key) // Similar to GetStringValue but returns the bytes directly. internal ReadOnlySpan GetStringBytesValue(string key) { - if (_jsonClaimsBytes.TryGetValue(key, out (int StartIndex, int Length)? location)) + if (_jsonClaims.TryGetValue(key, out object obj)) { - if (!location.HasValue) + if (obj == null) return null; - return _tokenAsMemory.Slice(location.Value.StartIndex, location.Value.Length).Span; + if (obj is ClaimPosition position) + return _tokenAsMemory.Slice(position.StartIndex, position.Length).Span; } - return new Span(); + return []; } #endif @@ -507,11 +491,7 @@ internal bool TryGetValue(string key, out T value) internal bool HasClaim(string claimName) { -#if NET8_0_OR_GREATER - return _jsonClaims.ContainsKey(claimName) || _jsonClaimsBytes.ContainsKey(claimName); -#else return _jsonClaims.ContainsKey(claimName); -#endif } } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs index 0f40a709ba..7f6b3b6f83 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs @@ -37,18 +37,12 @@ internal JsonClaimSet CreateHeaderClaimSet(Memory tokenHeaderAsMemory) LogHelper.MarkAsNonPII(reader.BytesConsumed)))); Dictionary claims = []; -#if NET8_0_OR_GREATER - Dictionary claimsBytes = []; -#endif while (true) { if (reader.TokenType == JsonTokenType.PropertyName) { -#if NET8_0_OR_GREATER - ReadHeaderValue(ref reader, claims, claimsBytes, tokenHeaderAsMemory); -#else - ReadHeaderValue(ref reader, claims); -#endif + 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)) @@ -58,93 +52,73 @@ internal JsonClaimSet CreateHeaderClaimSet(Memory tokenHeaderAsMemory) }; #if NET8_0_OR_GREATER - return new JsonClaimSet(claims, claimsBytes, tokenHeaderAsMemory); + return new JsonClaimSet(claims, tokenHeaderAsMemory); #else return new JsonClaimSet(claims); #endif } - private protected virtual void ReadHeaderValue(ref Utf8JsonReader reader, IDictionary claims) + /// + /// + /// + /// + /// + /// + public static object ReadTokenHeaderValue(ref Utf8JsonReader reader, string claimName) { - _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); - +#if NET8_0_OR_GREATER if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) { - _alg = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); - claims[JwtHeaderParameterNames.Alg] = _alg; + return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Cty)) { - _cty = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); - claims[JwtHeaderParameterNames.Cty] = _cty; + return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Kid)) { - _kid = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); - claims[JwtHeaderParameterNames.Kid] = _kid; + return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Typ)) { - _typ = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); - claims[JwtHeaderParameterNames.Typ] = _typ; + return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.X5t)) { - _x5t = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); - claims[JwtHeaderParameterNames.X5t] = _x5t; + return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Zip)) { - _zip = JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); - claims[JwtHeaderParameterNames.Zip] = _zip; + return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); } - else - { - string propertyName = reader.GetString(); - claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); - } - } - -#if NET8_0_OR_GREATER - private protected virtual void ReadHeaderValue( - ref Utf8JsonReader reader, - Dictionary claims, - Dictionary claimsBytes, - Memory tokenAsMemory) - { - _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); - _ = claimsBytes ?? throw LogHelper.LogArgumentNullException(nameof(claimsBytes)); - +#else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) { - claimsBytes[JwtHeaderParameterNames.Alg] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Alg, ClassName, true); + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Cty)) { - claimsBytes[JwtHeaderParameterNames.Cty] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Cty, ClassName, true); + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Kid)) { - claimsBytes[JwtHeaderParameterNames.Kid] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Kid, ClassName, true); + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Typ)) { - claimsBytes[JwtHeaderParameterNames.Typ] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Typ, ClassName, true); + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.X5t)) { - claimsBytes[JwtHeaderParameterNames.X5t] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.X5t, ClassName, true); + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Zip)) { - claimsBytes[JwtHeaderParameterNames.Zip] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtHeaderParameterNames.Zip, ClassName, true); + return JsonSerializerPrimitives.ReadString(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); } - else - { - string propertyName = reader.GetString(); - claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.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 77a817e29c..0dc13b225a 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 @@ -39,7 +38,7 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) if (reader.TokenType == JsonTokenType.PropertyName) { string claimName = reader.GetString(); - claims[claimName] = ReadTokenPayloadValue(ref reader, claimName); + 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)) @@ -48,7 +47,11 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) break; }; +#if NET8_0_OR_GREATER + return new JsonClaimSet(claims, tokenPayloadAsMemory); +#else return new JsonClaimSet(claims); +#endif } /* @@ -70,148 +73,73 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) */ // Default implementation // Read the value of the claimName from the reader into the dictionary. - public static object ReadPayloadValue(ref Utf8JsonReader reader, string claimName) + /// + /// + /// + /// + /// + /// + 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; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Exp)) - { - _exp = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true); - _expDateTime = EpochTime.DateTime(_exp.Value); - claims[JwtRegisteredClaimNames.Exp] = _exp; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iat)) - { - _iat = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true); - _iatDateTime = EpochTime.DateTime(_iat.Value); - claims[JwtRegisteredClaimNames.Iat] = _iat; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss)) - { - _iss = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); - claims[JwtRegisteredClaimNames.Iss] = _iss; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti)) - { - _jti = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); - claims[JwtRegisteredClaimNames.Jti] = _jti; - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Nbf)) - { - _nbf = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true); - _nbfDateTime = EpochTime.DateTime(_nbf.Value); - claims[JwtRegisteredClaimNames.Nbf] = _nbf; - } - 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); - } - } - #if NET8_0_OR_GREATER - private protected virtual void ReadPayloadValue( - ref Utf8JsonReader reader, - Dictionary claims, - Dictionary claimsBytes, - Memory tokenAsMemory) - { - _ = claims ?? throw LogHelper.LogArgumentNullException(nameof(claims)); - _ = claimsBytes ?? throw LogHelper.LogArgumentNullException(nameof(claimsBytes)); - - if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) - { - _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; - } - } - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) - { - claimsBytes[JwtRegisteredClaimNames.Azp] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Azp, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytesLocation(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)) { - claimsBytes[JwtRegisteredClaimNames.Iss] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Iss, ClassName, true); +#if NET8_0_OR_GREATER + return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); +#else + return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); +#endif } else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti)) { - claimsBytes[JwtRegisteredClaimNames.Jti] = JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, tokenAsMemory, JwtRegisteredClaimNames.Jti, ClassName, true); +#if NET8_0_OR_GREATER + return JsonSerializerPrimitives.ReadStringBytesLocation(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); } -#endif } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 0b8dfabec8..ea774a0623 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -90,10 +90,15 @@ public JsonWebToken(string jwtEncodedString) /// /// /// + /// /// - public JsonWebToken(ReadOnlyMemory encodedTokenMemory, ReadTokenPayloadValue readTokenPayloadValueDelegate) : this(encodedTokenMemory) + public JsonWebToken( + ReadOnlyMemory encodedTokenMemory, + ReadTokenHeaderValueDelegate readTokenHeaderValueDelegate, + ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) : this(encodedTokenMemory) { - ReadTokenPayloadValue = readTokenPayloadValueDelegate; + ReadTokenHeaderValueDelegate = readTokenHeaderValueDelegate; + ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate; } /// @@ -153,9 +158,15 @@ public JsonWebToken(string header, string payload) } /// - /// + /// Called for each claim when token header is being read. + /// + internal ReadTokenHeaderValueDelegate ReadTokenHeaderValueDelegate { get; set; } = ReadTokenHeaderValue; + + + /// + /// Called for each claim when token payload is being read. /// - internal ReadTokenPayloadValue ReadTokenPayloadValue { get; set; } = ReadPayloadValue; + internal ReadTokenPayloadValueDelegate ReadTokenPayloadValueDelegate { get; set; } = ReadTokenPayloadValue; internal string ActualIssuer { get; set; } diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 479d198116..a4a977b3f8 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -179,10 +179,20 @@ namespace Microsoft.IdentityModel.Tokens public delegate SecurityToken TransformBeforeSignatureValidation(SecurityToken token, TokenValidationParameters validationParameters); /// - /// Definition for ReadTokenPayloadValue. + /// 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 ReadTokenPayloadValue(ref Utf8JsonReader reader, string claimName); + 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/ClaimPosition.cs b/src/Microsoft.IdentityModel.Tokens/Json/ClaimPosition.cs new file mode 100644 index 0000000000..a4d059a970 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Json/ClaimPosition.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.IdentityModel.Tokens.Json +{ + /// + /// Represents the position of the claim value in the token bytes + /// + /// The start index of the claim value (not including the quotes). + /// The length of the claim value (not including the quotes). + /// Indicates if the value bytes are escaped and need to be unescaped before returning the claim value. + internal class ClaimPosition(int startIndex, int length, bool isEscaped) + { + /// + /// The start index of the claim value (not including the quotes). + /// + public int StartIndex { get; set; } = startIndex; + + /// + /// The length of the claim value (not including the quotes). + /// + public int Length { get; set; } = length; + + /// + /// Indicates if the value bytes are escaped and need to be unescaped before returning the claim value. + /// + public bool IsEscaped { get; set; } = isEscaped; + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index 2aa3e77f7b..6ffeb37260 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -656,10 +656,9 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName } #if NET8_0_OR_GREATER - // Mostly the same as ReadString, but this method returns the location of the string in the token. - internal static (int StartIndex, int Length)? ReadStringBytesLocation( + // Mostly the same as ReadString, but this method returns the position of the claim value in the token bytes. + internal static ClaimPosition ReadStringBytesLocation( ref Utf8JsonReader reader, - Memory tokenAsMemory, string propertyName, string className, bool read = false) @@ -672,21 +671,12 @@ internal static (int StartIndex, int Length)? ReadStringBytesLocation( throw LogHelper.LogExceptionMessage( CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); - if (reader.ValueIsEscaped) - { - // Escaped string is always longer than unescaped - // Unescapes in-place - int bytesWritten = reader.CopyString(tokenAsMemory.Span.Slice((int)reader.TokenStartIndex, reader.ValueSpan.Length)); - - return ((int)reader.TokenStartIndex, bytesWritten); - } - - var location = ((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length); + var claimPosition = new ClaimPosition((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length, reader.ValueIsEscaped); // Move to next token reader.Read(); - return location; + return claimPosition; } #endif diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 6d1a3bfedb..5e3eb9da8b 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -449,7 +449,12 @@ public string NameClaimType /// /// Gets or sets a delegate that will be called when reading token payload claims. /// - public ReadTokenPayloadValue ReadTokenPayloadValue { get; set; } + 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. 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 d3f6dd5801..0b4af0752b 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -1771,7 +1771,7 @@ 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 From c86355e62a4fdb5f77b3252faf67c7592e190425 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 7 Aug 2024 02:36:16 -0700 Subject: [PATCH 17/24] Add test. Update comments. --- .../Json/JsonWebToken.HeaderClaimSet.cs | 8 +-- .../Json/JsonWebToken.PayloadClaimSet.cs | 27 ++-------- .../JsonWebToken.cs | 48 +++++++++++++++--- .../JsonWebTokenTests.cs | 49 ++++++++++++++++--- 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs index 7f6b3b6f83..22efe858ef 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs @@ -59,11 +59,11 @@ internal JsonClaimSet CreateHeaderClaimSet(Memory tokenHeaderAsMemory) } /// - /// + /// 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 diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 2e3122edd4..ef535d6e6f 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -57,31 +57,12 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) #endif } - /* - * Custom implemenation - ReadPayloadValueDelegate(ref Utf8JsonReader reader, string claimName) => - { - if (claimName == JwtRegisteredClaimNames.CustomProp) - { - return JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true); - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.CustomProp)) - { - return JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true); - } else - { - return JsonWebToken.ReadPayloadValue(ref reader, claimName); - } - } - */ - // Default implementation - // Read the value of the claimName from the reader into the dictionary. /// - /// + /// 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)) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index eae4e1f9d6..cc6e1ff7d0 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -87,18 +87,26 @@ public JsonWebToken(string 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) : this(encodedTokenMemory) + ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) { - ReadTokenHeaderValueDelegate = readTokenHeaderValueDelegate; - ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate; + if (encodedTokenMemory.IsEmpty) + throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(encodedTokenMemory))); + + ReadTokenHeaderValueDelegate = readTokenHeaderValueDelegate ?? ReadTokenHeaderValue; + ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue; + + ReadToken(encodedTokenMemory); + + _encodedTokenMemory = encodedTokenMemory; + } /// @@ -159,12 +167,38 @@ public JsonWebToken(string header, string payload) /// /// 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; } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 455453badb..ed62ef62e3 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -13,9 +13,11 @@ using System.Security.Claims; using System.Security.Cryptography; 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; @@ -1794,20 +1796,53 @@ public void StringAndMemoryConstructors_CreateEquivalentTokens(JwtTheoryData the [Fact] 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(); - var derivedToken = new CustomJsonWebToken(tokenStr); + 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); + } - Assert.Equal(expectedCustomClaim, derivedToken.CustomClaim); - Assert.Equal(Default.Issuer, derivedToken.Issuer); + 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); + } + + 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; } } From f22e584c9cdf9ac013eceed00f7f4bbabffbc3b8 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Thu, 8 Aug 2024 01:46:34 -0700 Subject: [PATCH 18/24] Add code to unscape values. --- .../Json/JsonClaimSet.cs | 26 ++++++++++++++++--- .../Json/JsonSerializerPrimitives.cs | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 0d6047d820..692e54fe66 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -7,7 +7,6 @@ using System.Collections.ObjectModel; using System.Globalization; using System.Security.Claims; -using System.Text; using System.Text.Json; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; @@ -75,7 +74,10 @@ internal List CreateClaims(string issuer) #if NET8_0_OR_GREATER if (kvp.Value is ClaimPosition position) { - string value = Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); + if (position.IsEscaped) + EscapeStringBytes(position); + + string value = System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); claims.Add(new Claim(kvp.Key, value, ClaimValueTypes.String, issuer, issuer)); } #endif @@ -198,7 +200,12 @@ internal string GetStringValue(string key) #if NET8_0_OR_GREATER if (obj is ClaimPosition position) - return Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); + { + if (position.IsEscaped) + EscapeStringBytes(position); + + return System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); + } #endif return obj.ToString(); } @@ -215,11 +222,22 @@ internal ReadOnlySpan GetStringBytesValue(string key) return null; if (obj is ClaimPosition position) + { + if (position.IsEscaped) + EscapeStringBytes(position); + return _tokenAsMemory.Slice(position.StartIndex, position.Length).Span; + } } return []; } + + private void EscapeStringBytes(ClaimPosition position) + { + position.Length = new Utf8JsonReader(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)).CopyString(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)); + position.IsEscaped = false; + } #endif internal DateTime GetDateTime(string key) @@ -480,7 +498,7 @@ internal bool TryGetValue(string key, out T value) var span = GetStringBytesValue(key); if (!span.IsEmpty) { - value = (T)(object)Encoding.UTF8.GetString(span); + value = (T)(object)System.Text.Encoding.UTF8.GetString(span); return true; } } diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index 2f8ec21335..b6fd36ad0b 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -655,6 +655,7 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName #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 ClaimPosition ReadStringBytesLocation( ref Utf8JsonReader reader, string propertyName, From 5b8c869cf676c9c4f4a9ccf6e7f76e4b182b2f03 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:30:06 -0700 Subject: [PATCH 19/24] Fixes. --- .../Json/JsonClaimSet.cs | 8 +++---- .../JsonWebToken.cs | 1 - .../Json/JsonSerializerPrimitives.cs | 6 ++--- .../{ClaimPosition.cs => ValuePosition.cs} | 2 +- .../JsonWebTokenTests.cs | 24 ++++--------------- 5 files changed, 13 insertions(+), 28 deletions(-) rename src/Microsoft.IdentityModel.Tokens/Json/{ClaimPosition.cs => ValuePosition.cs} (94%) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 692e54fe66..6c8d7add9f 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -72,7 +72,7 @@ internal List CreateClaims(string issuer) CreateClaimFromObject(claims, kvp.Key, kvp.Value, issuer); #if NET8_0_OR_GREATER - if (kvp.Value is ClaimPosition position) + if (kvp.Value is ValuePosition position) { if (position.IsEscaped) EscapeStringBytes(position); @@ -199,7 +199,7 @@ internal string GetStringValue(string key) return null; #if NET8_0_OR_GREATER - if (obj is ClaimPosition position) + if (obj is ValuePosition position) { if (position.IsEscaped) EscapeStringBytes(position); @@ -221,7 +221,7 @@ internal ReadOnlySpan GetStringBytesValue(string key) if (obj == null) return null; - if (obj is ClaimPosition position) + if (obj is ValuePosition position) { if (position.IsEscaped) EscapeStringBytes(position); @@ -233,7 +233,7 @@ internal ReadOnlySpan GetStringBytesValue(string key) return []; } - private void EscapeStringBytes(ClaimPosition position) + private void EscapeStringBytes(ValuePosition position) { position.Length = new Utf8JsonReader(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)).CopyString(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)); position.IsEscaped = false; diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index cc6e1ff7d0..a175f08187 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Buffers; using System.Collections.Generic; using System.Security.Claims; using System.Text; diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index b6fd36ad0b..c720ab9e3b 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -656,7 +656,7 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName #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 ClaimPosition ReadStringBytesLocation( + internal static ValuePosition ReadStringBytesLocation( ref Utf8JsonReader reader, string propertyName, string className, @@ -670,7 +670,7 @@ internal static ClaimPosition ReadStringBytesLocation( throw LogHelper.LogExceptionMessage( CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); - var claimPosition = new ClaimPosition((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length, reader.ValueIsEscaped); + var claimPosition = new ValuePosition((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length, reader.ValueIsEscaped); // Move to next token reader.Read(); @@ -1081,7 +1081,7 @@ private static bool IsReaderPositionedOnNull(ref Utf8JsonReader reader, bool rea return true; } -#endregion + #endregion #region Write public static void WriteAsJsonElement(ref Utf8JsonWriter writer, string json) diff --git a/src/Microsoft.IdentityModel.Tokens/Json/ClaimPosition.cs b/src/Microsoft.IdentityModel.Tokens/Json/ValuePosition.cs similarity index 94% rename from src/Microsoft.IdentityModel.Tokens/Json/ClaimPosition.cs rename to src/Microsoft.IdentityModel.Tokens/Json/ValuePosition.cs index a4d059a970..b41e28ce83 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/ClaimPosition.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/ValuePosition.cs @@ -9,7 +9,7 @@ namespace Microsoft.IdentityModel.Tokens.Json /// The start index of the claim value (not including the quotes). /// The length of the claim value (not including the quotes). /// Indicates if the value bytes are escaped and need to be unescaped before returning the claim value. - internal class ClaimPosition(int startIndex, int length, bool isEscaped) + internal class ValuePosition(int startIndex, int length, bool isEscaped) { /// /// The start index of the claim value (not including the quotes). diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index ed62ef62e3..4f761f0456 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -11,7 +11,6 @@ using System.Linq; using System.Reflection; using System.Security.Claims; -using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; @@ -1548,17 +1547,11 @@ public static TheoryData ParseTimeValuesTheoryData [Fact] public void ParseToken_WithByteProperties() { - // Arrange var escapedAzp = "azp\u0027azp"; var unescapedAzp = "azp'azp"; - RSA rsa = RSA.Create(2048); - RSAParameters rsaParameters = rsa.ExportParameters(true); - RsaSecurityKey rsaSecurityKey = new(rsaParameters) { KeyId = "RsaPrivate" }; - - var jwsTokenDescriptor = new SecurityTokenDescriptor + var tokenStr = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor { - SigningCredentials = new(rsaSecurityKey, SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Sha256), TokenType = JwtHeaderParameterNames.Jwk, Claims = new Dictionary() { @@ -1571,23 +1564,16 @@ public void ParseToken_WithByteProperties() { JwtRegisteredClaimNames.Jti, Default.Jti }, { "unknown_claim", "unknown_claim_value" }, } - }; - - var tokenStr = new JsonWebTokenHandler().CreateToken(jwsTokenDescriptor); + }); - // Act var jwt = new JsonWebToken(tokenStr); - jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Iss, out string issuerFromPayload); - jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Jti, out string jtiFromPayload); - jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Azp, out string azpFromPayload); - // Assert + Assert.True(jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Iss, out string issuerFromPayload)); Assert.Equal(Default.Issuer, issuerFromPayload); Assert.Equal(Default.Issuer, jwt.Issuer); Assert.True(jwt.IssuerBytes.SequenceEqual(Encoding.UTF8.GetBytes(Default.Issuer))); - Assert.Equal(Default.Jti, jtiFromPayload); - Assert.Equal(Default.Jti, jwt.Id); - Assert.True(jwt.IdBytes.SequenceEqual(Encoding.UTF8.GetBytes(Default.Jti))); + + Assert.True(jwt.TryGetPayloadValue(JwtRegisteredClaimNames.Azp, out string azpFromPayload)); Assert.Equal(unescapedAzp, azpFromPayload); Assert.Equal(unescapedAzp, jwt.Azp); Assert.True(jwt.AzpBytes.SequenceEqual(Encoding.UTF8.GetBytes(unescapedAzp))); From 1e736096345c550d561acf9a241ebb5d76e55679 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:13:03 -0700 Subject: [PATCH 20/24] Update reading escaped values. Update test. --- .../Json/JsonClaimSet.cs | 29 +++++++++++++++---- .../JsonWebTokenTests.cs | 20 +++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 6c8d7add9f..436b22b02a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -75,7 +75,7 @@ internal List CreateClaims(string issuer) if (kvp.Value is ValuePosition position) { if (position.IsEscaped) - EscapeStringBytes(position); + EscapeStringBytesInPlace(position); string value = System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); claims.Add(new Claim(kvp.Key, value, ClaimValueTypes.String, issuer, issuer)); @@ -202,7 +202,7 @@ internal string GetStringValue(string key) if (obj is ValuePosition position) { if (position.IsEscaped) - EscapeStringBytes(position); + EscapeStringBytesInPlace(position); return System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); } @@ -224,7 +224,7 @@ internal ReadOnlySpan GetStringBytesValue(string key) if (obj is ValuePosition position) { if (position.IsEscaped) - EscapeStringBytes(position); + EscapeStringBytesInPlace(position); return _tokenAsMemory.Slice(position.StartIndex, position.Length).Span; } @@ -233,9 +233,17 @@ internal ReadOnlySpan GetStringBytesValue(string key) return []; } - private void EscapeStringBytes(ValuePosition position) + /// + /// Unescapes the bytes of a string claim value in-place in the token bytes Memory instance. + /// After escaping, updates the length of the claim value to reflect the unescaped bytes. + /// + /// The start position and length provided to the Utf8JsonReader has to be adjusted to include double quotes. + /// Position of the claim value. + private void EscapeStringBytesInPlace(ValuePosition position) { - position.Length = new Utf8JsonReader(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)).CopyString(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)); + var reader = new Utf8JsonReader(_tokenAsMemory.Span.Slice(position.StartIndex - 1, position.Length + 2)); + reader.Read(); + position.Length = reader.CopyString(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)); position.IsEscaped = false; } #endif @@ -302,8 +310,19 @@ 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 ValuePosition position) + { + if (position.IsEscaped) + EscapeStringBytesInPlace(position); + + return (T)(object)System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); + } +#endif else + { return (T)((object)obj.ToString()); + } } else if (typeof(T) == typeof(bool)) { diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 4f761f0456..507c87839b 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -1545,11 +1545,13 @@ public static TheoryData ParseTimeValuesTheoryData #if NET8_0_OR_GREATER [Fact] - public void ParseToken_WithByteProperties() + public void ParseToken_EscapedAndUnescaped_PropertiesCorrectlySet() { - var escapedAzp = "azp\u0027azp"; - var unescapedAzp = "azp'azp"; + 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, @@ -1560,23 +1562,31 @@ public void ParseToken_WithByteProperties() { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(Default.IssueInstant) }, { JwtRegisteredClaimNames.Iss, Default.Issuer }, { JwtRegisteredClaimNames.Aud, Default.Audience }, - { JwtRegisteredClaimNames.Azp, escapedAzp }, + { JwtRegisteredClaimNames.Azp, unescapedAzp }, { JwtRegisteredClaimNames.Jti, Default.Jti }, - { "unknown_claim", "unknown_claim_value" }, + { 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 From f013da8c5496108c80b3776b450da9240790065c Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:10:32 -0700 Subject: [PATCH 21/24] Add benchmark to test validation with an issuer delegate using string vs bytes issuer. --- .../ValidateTokenAsyncTests.cs | 67 +++++++++++++++---- .../TokenValidationParameters.cs | 15 ++++- 2 files changed, 69 insertions(+), 13 deletions(-) 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.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index a2f22e8bda..6e2bdaca83 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; @@ -719,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) : null; + } + } + + internal ReadOnlyMemory ValidIssuerBytes { get; private set; } /// /// Gets or sets the that contains valid issuers that will be used to check against the token's issuer. From c9d257f6ef84c6bbb5dad0737bc90215d1a153d9 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:38:25 -0700 Subject: [PATCH 22/24] Add a class with profiler methods. --- .../ProfilerRuns.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 benchmark/Microsoft.IdentityModel.Benchmarks/ProfilerRuns.cs 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); + } + } + } +} From 75ebb5aa661ba201399888fdcfbcab27431304b3 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Fri, 13 Sep 2024 00:27:38 -0700 Subject: [PATCH 23/24] Default TVP IssuerBytes to an empty Span. --- .../TokenValidationParameters.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 6e2bdaca83..5b8c7ad59c 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -732,11 +732,11 @@ public string ValidIssuer set { _validIssuer = value; - ValidIssuerBytes = value != null ? Encoding.UTF8.GetBytes(value) : null; + ValidIssuerBytes = value != null ? Encoding.UTF8.GetBytes(value) : ReadOnlyMemory.Empty; } } - internal ReadOnlyMemory ValidIssuerBytes { get; private set; } + 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. From ba4b49f87abef64f99863899da8142e175aa2caa Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:06:06 -0700 Subject: [PATCH 24/24] Revert to use ArrayPool for the token bytes. Save bytes only for string properties. --- .../Json/JsonClaimSet.cs | 64 +++---------------- .../Json/JsonWebToken.HeaderClaimSet.cs | 24 +++---- .../Json/JsonWebToken.PayloadClaimSet.cs | 18 ++---- .../JsonWebToken.cs | 16 +++-- .../Json/JsonSerializerPrimitives.cs | 8 ++- .../Json/ValuePosition.cs | 29 --------- 6 files changed, 42 insertions(+), 117 deletions(-) delete mode 100644 src/Microsoft.IdentityModel.Tokens/Json/ValuePosition.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 436b22b02a..dd576d95e0 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -25,19 +25,11 @@ internal class JsonClaimSet internal object _claimsLock = new(); internal readonly Dictionary _jsonClaims; -#if NET8_0_OR_GREATER - internal readonly Memory _tokenAsMemory; -#endif - private List _claims; internal JsonClaimSet() { _jsonClaims = []; - -#if NET8_0_OR_GREATER - _tokenAsMemory = Memory.Empty; -#endif } internal JsonClaimSet(Dictionary jsonClaims) @@ -45,16 +37,6 @@ internal JsonClaimSet(Dictionary jsonClaims) _jsonClaims = jsonClaims; } -#if NET8_0_OR_GREATER - internal JsonClaimSet( - Dictionary jsonClaims, - Memory tokenAsMemory) - { - _jsonClaims = jsonClaims; - _tokenAsMemory = tokenAsMemory; - } -#endif - internal List Claims(string issuer) { if (_claims == null) @@ -72,12 +54,9 @@ internal List CreateClaims(string issuer) CreateClaimFromObject(claims, kvp.Key, kvp.Value, issuer); #if NET8_0_OR_GREATER - if (kvp.Value is ValuePosition position) + if (kvp.Value is Memory bytes) { - if (position.IsEscaped) - EscapeStringBytesInPlace(position); - - string value = System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); + string value = System.Text.Encoding.UTF8.GetString(bytes.Span); claims.Add(new Claim(kvp.Key, value, ClaimValueTypes.String, issuer, issuer)); } #endif @@ -199,13 +178,8 @@ internal string GetStringValue(string key) return null; #if NET8_0_OR_GREATER - if (obj is ValuePosition position) - { - if (position.IsEscaped) - EscapeStringBytesInPlace(position); - - return System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); - } + if (obj is Memory bytes) + return System.Text.Encoding.UTF8.GetString(bytes.Span); #endif return obj.ToString(); } @@ -221,31 +195,12 @@ internal ReadOnlySpan GetStringBytesValue(string key) if (obj == null) return null; - if (obj is ValuePosition position) - { - if (position.IsEscaped) - EscapeStringBytesInPlace(position); - - return _tokenAsMemory.Slice(position.StartIndex, position.Length).Span; - } + if (obj is Memory bytes) + return bytes.Span; } return []; } - - /// - /// Unescapes the bytes of a string claim value in-place in the token bytes Memory instance. - /// After escaping, updates the length of the claim value to reflect the unescaped bytes. - /// - /// The start position and length provided to the Utf8JsonReader has to be adjusted to include double quotes. - /// Position of the claim value. - private void EscapeStringBytesInPlace(ValuePosition position) - { - var reader = new Utf8JsonReader(_tokenAsMemory.Span.Slice(position.StartIndex - 1, position.Length + 2)); - reader.Read(); - position.Length = reader.CopyString(_tokenAsMemory.Span.Slice(position.StartIndex, position.Length)); - position.IsEscaped = false; - } #endif internal DateTime GetDateTime(string key) @@ -311,12 +266,9 @@ internal T GetValue(string key, bool throwEx, out bool found) return (T)((object)(list[0])); } #if NET8_0_OR_GREATER - else if (obj is ValuePosition position) + else if (obj is Memory bytes) { - if (position.IsEscaped) - EscapeStringBytesInPlace(position); - - return (T)(object)System.Text.Encoding.UTF8.GetString(_tokenAsMemory.Slice(position.StartIndex, position.Length).Span); + return (T)(object)System.Text.Encoding.UTF8.GetString(bytes.Span); } #endif else diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs index 22efe858ef..b77964093b 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs @@ -13,17 +13,17 @@ public partial class JsonWebToken { internal JsonClaimSet CreateHeaderClaimSet(byte[] bytes) { - return CreateHeaderClaimSet(bytes.AsMemory()); + return CreateHeaderClaimSet(bytes.AsSpan()); } internal JsonClaimSet CreateHeaderClaimSet(byte[] bytes, int length) { - return CreateHeaderClaimSet(bytes.AsMemory(0, length)); + return CreateHeaderClaimSet(bytes.AsSpan(0, length)); } - internal JsonClaimSet CreateHeaderClaimSet(Memory tokenHeaderAsMemory) + internal JsonClaimSet CreateHeaderClaimSet(ReadOnlySpan byteSpan) { - Utf8JsonReader reader = new(tokenHeaderAsMemory.Span); + Utf8JsonReader reader = new(byteSpan); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, true)) throw LogHelper.LogExceptionMessage( new JsonException( @@ -51,11 +51,7 @@ internal JsonClaimSet CreateHeaderClaimSet(Memory tokenHeaderAsMemory) break; }; -#if NET8_0_OR_GREATER - return new JsonClaimSet(claims, tokenHeaderAsMemory); -#else return new JsonClaimSet(claims); -#endif } /// @@ -69,27 +65,27 @@ public static object ReadTokenHeaderValue(ref Utf8JsonReader reader, string clai #if NET8_0_OR_GREATER if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) { - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Alg, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Cty)) { - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Cty, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Kid)) { - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Kid, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Typ)) { - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Typ, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.X5t)) { - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.X5t, ClassName, true); } else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Zip)) { - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtHeaderParameterNames.Zip, ClassName, true); } #else if (reader.ValueTextEquals(JwtHeaderUtf8Bytes.Alg)) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index ef535d6e6f..469db840ea 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -13,15 +13,15 @@ public partial class JsonWebToken { internal JsonClaimSet CreatePayloadClaimSet(byte[] bytes, int length) { - return CreatePayloadClaimSet(bytes.AsMemory(0, length)); + return CreatePayloadClaimSet(bytes.AsSpan(0, length)); } - internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) + internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) { - if (tokenPayloadAsMemory.Length == 0) + if (byteSpan.Length == 0) return new JsonClaimSet([]); - Utf8JsonReader reader = new(tokenPayloadAsMemory.Span); + Utf8JsonReader reader = new(byteSpan); if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, true)) throw LogHelper.LogExceptionMessage( new JsonException( @@ -50,11 +50,7 @@ internal JsonClaimSet CreatePayloadClaimSet(Memory tokenPayloadAsMemory) break; }; -#if NET8_0_OR_GREATER - return new JsonClaimSet(claims, tokenPayloadAsMemory); -#else return new JsonClaimSet(claims); -#endif } /// @@ -85,7 +81,7 @@ public static object ReadTokenPayloadValue(ref Utf8JsonReader reader, string cla else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) { #if NET8_0_OR_GREATER - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); #else return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true); #endif @@ -101,7 +97,7 @@ public static object ReadTokenPayloadValue(ref Utf8JsonReader reader, string cla else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss)) { #if NET8_0_OR_GREATER - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); #else return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true); #endif @@ -109,7 +105,7 @@ public static object ReadTokenPayloadValue(ref Utf8JsonReader reader, string cla else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti)) { #if NET8_0_OR_GREATER - return JsonSerializerPrimitives.ReadStringBytesLocation(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); + return JsonSerializerPrimitives.ReadStringBytes(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); #else return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true); #endif diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index a175f08187..e95d2598ec 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Buffers; using System.Collections.Generic; using System.Security.Claims; using System.Text; @@ -561,7 +562,7 @@ internal void ReadToken(ReadOnlyMemory encodedTokenMemory) try { - Header = CreateHeaderClaimSet(Base64UrlEncoder.Decode(headerSpan).AsMemory()); + Header = CreateHeaderClaimSet(Base64UrlEncoder.Decode(headerSpan).AsSpan()); } catch (Exception ex) { @@ -628,9 +629,16 @@ internal JsonClaimSet CreateClaimSet(ReadOnlySpan strSpan, int startIndex, { int outputSize = Base64UrlEncoding.ValidateAndGetOutputSize(strSpan, startIndex, length); - byte[] output = new byte[outputSize]; - Base64UrlEncoder.Decode(strSpan.Slice(startIndex, length), output); - return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsMemory()) : CreatePayloadClaimSet(output.AsMemory()); + byte[] output = ArrayPool.Shared.Rent(outputSize); + try + { + Base64UrlEncoder.Decode(strSpan.Slice(startIndex, length), output); + return createHeaderClaimSet ? CreateHeaderClaimSet(output.AsSpan()) : CreatePayloadClaimSet(output.AsSpan()); + } + finally + { + ArrayPool.Shared.Return(output, true); + } } /// diff --git a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs index c720ab9e3b..2a6c0fedb5 100644 --- a/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs +++ b/src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs @@ -656,7 +656,7 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName #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 ValuePosition ReadStringBytesLocation( + internal static Memory? ReadStringBytes( ref Utf8JsonReader reader, string propertyName, string className, @@ -670,12 +670,14 @@ internal static ValuePosition ReadStringBytesLocation( throw LogHelper.LogExceptionMessage( CreateJsonReaderExceptionInvalidType(ref reader, "JsonTokenType.StartArray", className, propertyName)); - var claimPosition = new ValuePosition((int)reader.TokenStartIndex + 1, reader.ValueSpan.Length, reader.ValueIsEscaped); + + var stringBytes = new Memory(new byte[reader.ValueSpan.Length]); + reader.CopyString(stringBytes.Span); // Move to next token reader.Read(); - return claimPosition; + return stringBytes; } #endif diff --git a/src/Microsoft.IdentityModel.Tokens/Json/ValuePosition.cs b/src/Microsoft.IdentityModel.Tokens/Json/ValuePosition.cs deleted file mode 100644 index b41e28ce83..0000000000 --- a/src/Microsoft.IdentityModel.Tokens/Json/ValuePosition.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.IdentityModel.Tokens.Json -{ - /// - /// Represents the position of the claim value in the token bytes - /// - /// The start index of the claim value (not including the quotes). - /// The length of the claim value (not including the quotes). - /// Indicates if the value bytes are escaped and need to be unescaped before returning the claim value. - internal class ValuePosition(int startIndex, int length, bool isEscaped) - { - /// - /// The start index of the claim value (not including the quotes). - /// - public int StartIndex { get; set; } = startIndex; - - /// - /// The length of the claim value (not including the quotes). - /// - public int Length { get; set; } = length; - - /// - /// Indicates if the value bytes are escaped and need to be unescaped before returning the claim value. - /// - public bool IsEscaped { get; set; } = isEscaped; - } -}