From c2fa102c154ea99ee6215c083bfb006f370dbcc1 Mon Sep 17 00:00:00 2001 From: Brent Schmaltz Date: Fri, 29 Sep 2023 15:39:44 -0700 Subject: [PATCH] Decouple JsonElements from JsonDocument. --- .../Json/JsonClaimSet.cs | 30 ++--- .../JsonWebToken.cs | 22 +++- .../JsonWebTokenTests.cs | 112 ++++++++++++++++++ 3 files changed, 147 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index f38e7ba40a..581116a7f3 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -25,8 +25,8 @@ internal class JsonClaimSet internal JsonClaimSet(JsonDocument jsonDocument) { - RootElement = jsonDocument.RootElement.Clone(); - jsonDocument.Dispose(); + foreach (JsonProperty jsonProperty in jsonDocument.RootElement.EnumerateObject()) + Elements[jsonProperty.Name] = jsonProperty.Value.Clone(); } internal JsonClaimSet(byte[] jsonBytes) : this(JsonDocument.Parse(jsonBytes)) @@ -37,8 +37,6 @@ internal JsonClaimSet(string json) : this(JsonDocument.Parse(json)) { } - internal JsonElement RootElement { get; } - internal IList Claims(string issuer) { if (_claims == null) @@ -58,20 +56,20 @@ internal IList Claims(string issuer) internal IList CreateClaims(string issuer) { IList claims = new List(); - foreach (JsonProperty property in RootElement.EnumerateObject()) + foreach (KeyValuePair kvp in Elements) { - if (property.Value.ValueKind == JsonValueKind.Array) + if (kvp.Value.ValueKind == JsonValueKind.Array) { - foreach (JsonElement jsonElement in property.Value.EnumerateArray()) + foreach (JsonElement jsonElement in kvp.Value.EnumerateArray()) { - Claim claim = CreateClaimFromJsonElement(property.Name, issuer, jsonElement); + Claim claim = CreateClaimFromJsonElement(kvp.Key, issuer, jsonElement); if (claim != null) claims.Add(claim); } } else { - Claim claim = CreateClaimFromJsonElement(property.Name, issuer, property.Value); + Claim claim = CreateClaimFromJsonElement(kvp.Key, issuer, kvp.Value); if (claim != null) claims.Add(claim); } @@ -181,12 +179,14 @@ private static object CreateObjectFromJsonElement(JsonElement jsonElement) return jsonElement.GetString(); } + internal Dictionary Elements { get; } = new Dictionary(); + internal Claim GetClaim(string key, string issuer) { if (key == null) throw new ArgumentNullException(nameof(key)); - if (!RootElement.TryGetProperty(key, out JsonElement jsonElement)) + if (!Elements.TryGetValue(key, out JsonElement jsonElement)) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX14304, key))); return CreateClaimFromJsonElement(key, issuer, jsonElement); @@ -228,7 +228,7 @@ internal static string GetClaimValueType(object obj) internal string GetStringValue(string key) { - if (RootElement.TryGetProperty(key, out JsonElement jsonElement) && jsonElement.ValueKind == JsonValueKind.String) + if (Elements.TryGetValue(key, out JsonElement jsonElement) && jsonElement.ValueKind == JsonValueKind.String) return jsonElement.GetString(); return string.Empty; @@ -236,7 +236,7 @@ internal string GetStringValue(string key) internal DateTime GetDateTime(string key) { - if (!RootElement.TryGetProperty(key, out JsonElement jsonElement)) + if (!Elements.TryGetValue(key, out JsonElement jsonElement)) return DateTime.MinValue; return EpochTime.DateTime(Convert.ToInt64(Math.Truncate(Convert.ToDouble(ParseTimeValue(key, jsonElement), CultureInfo.InvariantCulture)))); @@ -260,7 +260,7 @@ internal T GetValue(string key) /// internal T GetValue(string key, bool throwEx, out bool found) { - found = RootElement.TryGetProperty(key, out JsonElement jsonElement); + found = Elements.TryGetValue(key, out JsonElement jsonElement); if (!found) { if (throwEx) @@ -359,7 +359,7 @@ internal T GetValue(string key, bool throwEx, out bool found) internal bool TryGetClaim(string key, string issuer, out Claim claim) { - if (!RootElement.TryGetProperty(key, out JsonElement jsonElement)) + if (!Elements.TryGetValue(key, out JsonElement jsonElement)) { claim = null; return false; @@ -387,7 +387,7 @@ internal bool TryGetValue(string key, out T value) internal bool HasClaim(string claimName) { - return RootElement.TryGetProperty(claimName, out _); + return Elements.TryGetValue(claimName, out _); } private static long ParseTimeValue(string claimName, JsonElement jsonElement) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index b1cd17f6cc..942532de6e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -455,6 +455,9 @@ private void ReadToken(string encodedJson) // Format: https://www.rfc-editor.org/rfc/rfc7515#page-7 IsSigned = !(Dot2 + 1 == encodedJson.Length); +#if !NET45 + JsonDocument jsonHeaderDocument = null; +#endif try { #if NET45 @@ -463,14 +466,22 @@ private void ReadToken(string encodedJson) _hChars = encodedJson.ToCharArray(0, Dot1); Header = new JsonClaimSet(Base64UrlEncoder.UnsafeDecode(_hChars)); #else - Header = new JsonClaimSet(JwtTokenUtilities.GetJsonDocumentFromBase64UrlEncodedString(encodedJson, 0, Dot1)); + jsonHeaderDocument = JwtTokenUtilities.GetJsonDocumentFromBase64UrlEncodedString(encodedJson, 0, Dot1); + Header = new JsonClaimSet(jsonHeaderDocument); #endif } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX14102, encodedJson.Substring(0, Dot1)), ex)); } +#if !NET45 + finally + { + jsonHeaderDocument?.Dispose(); + } + JsonDocument jsonPayloadDocument = null; +#endif try { #if NET45 @@ -478,13 +489,20 @@ private void ReadToken(string encodedJson) _pChars = encodedJson.ToCharArray(Dot1 + 1, Dot2 - Dot1 - 1); Payload = new JsonClaimSet(Base64UrlEncoder.UnsafeDecode(_pChars)); #else - Payload = new JsonClaimSet(JwtTokenUtilities.GetJsonDocumentFromBase64UrlEncodedString(encodedJson, Dot1 + 1, Dot2 - Dot1 - 1)); + jsonPayloadDocument = JwtTokenUtilities.GetJsonDocumentFromBase64UrlEncodedString(encodedJson, Dot1 + 1, Dot2 - Dot1 - 1); + Payload = new JsonClaimSet(jsonPayloadDocument); #endif } catch (Exception ex) { throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX14101, encodedJson.Substring(Dot2, Dot2 - Dot1)), ex)); } +#if !NET45 + finally + { + jsonPayloadDocument?.Dispose(); + } +#endif } else { diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 0a6941c796..c80ce297a3 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -18,6 +18,7 @@ using JsonReaderException = Microsoft.IdentityModel.Json.JsonReaderException; #else using System.Text.Json; +using System.Threading.Tasks; using JsonReaderException = System.Text.Json.JsonException; #endif @@ -43,6 +44,117 @@ public class JsonWebTokenTests new Claim("dateTimeIso8061", dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), ClaimValueTypes.DateTime, "LOCAL AUTHORITY", "LOCAL AUTHORITY"), }; +#if !NET452 + static string app_displayname = "sdtest-AllAccess"; + static string appid = "C9977FDF-DBB9-4E26-921B-639552F0F810"; + static string aud = "https://graph.microsoft.com"; + static string iss = "https://sts.windows.net/80165B57-9098-4710-B4AC-98E5D08DF51D/"; + static string given_name = "given_name"; + static string scp = "Calendars.Read Calendars.ReadWrite Contacts.Read Contacts.ReadWrite Directory.AccessAsUser.All Directory.Read.All Directory.ReadWrite.All email Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite Files.ReadWrite.All Files.ReadWrite.AppFolder Files.ReadWrite.Selected Group.Read.All Group.ReadWrite.All IdentityRiskEvent.Read.All Mail.Read Mail.ReadWrite Mail.Send MailboxSettings.ReadWrite Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp offline_access openid People.Read People.ReadWrite profile recipient.manage Sites.Read.All Tasks.ReadWrite User.Read User.Read.All User.ReadBasic.All User.ReadWrite User.ReadWrite.All"; + + [Fact] + public async Task JsonClaimSetThreading() + { + var document = JsonDocument.Parse(Payload, new JsonDocumentOptions { AllowTrailingCommas = true }); + JsonClaimSet jsonClaimSet = new JsonClaimSet(document); + + var taskList = new List(); + for (var i = 0; i < 1000000; i++) + { + var task = new Task(() => + { + CheckElement(jsonClaimSet, "app_displayname", app_displayname); + CheckElement(jsonClaimSet, "appid", appid); + CheckElement(jsonClaimSet, "aud", aud); + CheckElement(jsonClaimSet, "iss", iss); + CheckElement(jsonClaimSet, "scp", scp); + }); + + task.Start(); + taskList.Add(task); + } + + await Task.WhenAll(taskList).ConfigureAwait(false); + document.Dispose(); + } + + [Fact] + public async Task JsonWebTokenThreading() + { + JsonWebToken jwt = new JsonWebToken("{}", Payload); + + var taskList = new List(); + for (var i = 0; i < 1000000; i++) + { + var task = new Task(() => + { + CheckClaimValue(jwt, "app_displayname", app_displayname); + CheckClaimValue(jwt, "appid", appid); + CheckClaimValue(jwt, "aud", aud); + CheckClaimValue(jwt, "iss", iss); + CheckClaimValue(jwt, "scp", scp); + }); + + task.Start(); + taskList.Add(task); + } + + await Task.WhenAll(taskList).ConfigureAwait(false); + } + + internal void CheckClaimValue(JsonWebToken jwt, string claim, string expectedClaim) + { + bool success = jwt.TryGetPayloadValue(claim, out string strValue); + + Assert.True(success); + Assert.Equal(strValue, expectedClaim); + Assert.NotEqual(given_name, strValue); + } + + internal void CheckElement(JsonClaimSet jsonClaimSet, string claim, string expectedValue) + { + bool success = jsonClaimSet.TryGetValue(claim, out JsonElement jsonElement); + + Assert.True(success); + Assert.Equal(JsonValueKind.String, jsonElement.ValueKind); + Assert.Equal(expectedValue, jsonElement.GetString()); + Assert.NotEqual(given_name, jsonElement.GetString()); + } + + public static string Payload => + $@"{{ + ""aud"": ""{aud}"", + ""iss"": ""{iss}"", + ""iat"": 1506034341, + ""nbf"": 1506034341, + ""exp"": 1506038241, + ""acr"": ""1"", + ""aio"": ""80165B57-9098-4710-B4AC-98E5D08DF51D="", + ""amr"": [ + ""pwd"" + ], + ""app_displayname"": ""{app_displayname}"", + ""appid"": ""{appid}"", + ""appidacr"": ""1"", + ""family_name"": ""Doe"", + ""given_name"": ""{given_name}"", + ""ipaddr"": ""0.0.0.127"", + ""name"": ""TEST_TEST_NAME"", + ""oid"": ""462AAB4E-E470-4331-9B9F-2507319916A5"", + ""platf"": ""14"", + ""puid"": ""123456789ABC"", + ""scp"": ""{scp}"", + ""sub"": ""CA3F3BA1-14DA-4040-9408-FCDC5E4F714C"", + ""tid"": ""55C25696-89F7-42A6-8B31-6294BFDB377C"", + ""unique_name"": ""admin@A49052AB-06C5-4BC4-8263-67865A8267CB.net"", + ""upn"": ""A49052AB-06C5-4BC4-8263-67865A8267CB.net"", + ""uti"": ""80165B57-9098-4710-B4AC-98E5D08DF51D"", + ""ver"": ""1.0"", + ""wids"": [ + ""80165B57-9098-4710-B4AC-98E5D08DF51D"" + ] + }}"; +#endif // This test is designed to test that all properties of a JWE can be accessed. // Some properties rely on an inner token and the Payload can be null.