Skip to content

Commit

Permalink
Decouple JsonElements from JsonDocument.
Browse files Browse the repository at this point in the history
  • Loading branch information
Brent Schmaltz authored and brentschmaltz committed Oct 2, 2023
1 parent a8cbc38 commit c2fa102
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 17 deletions.
30 changes: 15 additions & 15 deletions src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -37,8 +37,6 @@ internal JsonClaimSet(string json) : this(JsonDocument.Parse(json))
{
}

internal JsonElement RootElement { get; }

internal IList<Claim> Claims(string issuer)
{
if (_claims == null)
Expand All @@ -58,20 +56,20 @@ internal IList<Claim> Claims(string issuer)
internal IList<Claim> CreateClaims(string issuer)
{
IList<Claim> claims = new List<Claim>();
foreach (JsonProperty property in RootElement.EnumerateObject())
foreach (KeyValuePair<string, JsonElement> 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);
}
Expand Down Expand Up @@ -181,12 +179,14 @@ private static object CreateObjectFromJsonElement(JsonElement jsonElement)
return jsonElement.GetString();
}

internal Dictionary<string, JsonElement> Elements { get; } = new Dictionary<string, JsonElement>();

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);
Expand Down Expand Up @@ -228,15 +228,15 @@ 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;
}

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))));
Expand All @@ -260,7 +260,7 @@ internal T GetValue<T>(string key)
/// <returns></returns>
internal T GetValue<T>(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)
Expand Down Expand Up @@ -359,7 +359,7 @@ internal T GetValue<T>(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;
Expand Down Expand Up @@ -387,7 +387,7 @@ internal bool TryGetValue<T>(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)
Expand Down
22 changes: 20 additions & 2 deletions src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -463,28 +466,43 @@ 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
MessageBytes = Encoding.UTF8.GetBytes(encodedJson.ToCharArray(0, Dot2));
_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
{
Expand Down
112 changes: 112 additions & 0 deletions test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<Task>();
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<Task>();
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"": ""[email protected]"",
""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.
Expand Down

0 comments on commit c2fa102

Please sign in to comment.