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