Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/JsonWebToken/Reader/JsonMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private void Enlarge()
private void AssertValidIndex(int index)
{
Debug.Assert(index >= 0);
Debug.Assert(index <= Length - JsonRow.Size, $"index {index} is out of bounds");
// Debug.Assert(index <= Length - JsonRow.Size, $"index {index} is out of bounds");
Debug.Assert(index % JsonRow.Size == 0, $"index {index} is not at a record start position");
}

Expand Down
5 changes: 5 additions & 0 deletions src/JsonWebToken/Reader/JsonRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ internal JsonRow(JsonTokenType jsonTokenType, int location, int sizeOrLength)
Debug.Assert(location >= 0);
Debug.Assert(sizeOrLength >= UnknownSize);

if (location < 0)
{
throw new System.Exception($"{location}");
}

_location = location;
_lengthUnion = sizeOrLength;
_numberOfItemsAndTypeUnion = (int)jsonTokenType << 28;
Expand Down
79 changes: 30 additions & 49 deletions src/JsonWebToken/Reader/JwtDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,16 @@ internal JwtDocument(ReadOnlyMemory<byte> utf8Json, JsonMetadata parsedData, byt
Debug.Assert(isDisposable || extraRentedBytes == null);
}

internal JwtDocument()
private JwtDocument()
{
_isDisposable = false;
_utf8Json = new byte[1];
_disposableRegistry = new List<IDisposable>(0);
_root = new JwtElement(this, 0);
_disposableRegistry = new List<IDisposable>(Array.Empty<IDisposable>());
_root = default;
}

internal bool TryGetNamedPropertyValue(ReadOnlySpan<char> propertyName, out JwtElement value)
{
JsonRow row;

int maxBytes = Utf8.GetMaxByteCount(propertyName.Length);
int endIndex = _parsedData.Length;

Expand All @@ -75,48 +73,25 @@ internal bool TryGetNamedPropertyValue(ReadOnlySpan<char> propertyName, out JwtE
utf8Name,
out value);
}

// Unescaping the property name will make the string shorter (or the same)
// So the first viable candidate is one whose length in bytes matches, or
// exceeds, our length in chars.
//
// The maximal escaping seems to be 6 -> 1 ("\u0030" => "0"), but just transcode
// and switch once one viable long property is found.
int minBytes = propertyName.Length;
for (int candidateIndex = 0; candidateIndex <= endIndex; candidateIndex += JsonRow.Size * 2)
else
{
row = _parsedData.Get(candidateIndex);
Debug.Assert(row.TokenType == JsonTokenType.PropertyName);

if (row.Length >= minBytes)
byte[] tmpUtf8 = ArrayPool<byte>.Shared.Rent(maxBytes);
Span<byte> utf8Name = tmpUtf8;
try
{
byte[] tmpUtf8 = ArrayPool<byte>.Shared.Rent(maxBytes);
Span<byte> utf8Name = default;

try
{
int len = JsonReaderHelper.GetUtf8FromText(propertyName, tmpUtf8);
utf8Name = tmpUtf8.AsSpan(0, len);
int len = JsonReaderHelper.GetUtf8FromText(propertyName, utf8Name);
utf8Name = utf8Name.Slice(0, len);

return TryGetNamedPropertyValue(
candidateIndex,
utf8Name,
out value);
}
finally
{
ArrayPool<byte>.Shared.Return(tmpUtf8);
}
return TryGetNamedPropertyValue(
endIndex,
utf8Name,
out value);
}
finally
{
ArrayPool<byte>.Shared.Return(tmpUtf8);
}
}

// None of the property names were within the range that the UTF-8 encoding would have been.
#if NET5_0_OR_GREATER
Unsafe.SkipInit(out value);
#else
value = default;
#endif
return false;
}

internal bool TryGetNamedPropertyValue(ReadOnlySpan<byte> propertyName, out JwtElement value)
Expand Down Expand Up @@ -280,6 +255,16 @@ private ReadOnlyMemory<byte> GetRawValue(int index, bool includeQuotes)
return _utf8Json.Slice(row.Location, row.Length);
}

if ((uint)row.Location > (uint)_utf8Json.Length)
{
throw new Exception($"{(uint)row.Location} > {(uint)_utf8Json.Length}");
}

if ((uint)row.Length > (uint)(_utf8Json.Length - row.Location))
{
throw new Exception($"{(uint)row.Length} > {(uint)_utf8Json.Length - row.Location}");
}

return _utf8Json.Slice(row.Location, row.Length);
}

Expand Down Expand Up @@ -590,20 +575,17 @@ internal int GetMemberCount(int index)

internal JwtElement GetArrayIndexElement(int currentIndex, int arrayIndex)
{
JsonRow row = _parsedData.Get(currentIndex);

CheckExpectedType(JsonTokenType.StartArray, row.TokenType);

int arrayLength = row.Length;
int arrayLength = _parsedData.Count;

if ((uint)arrayIndex >= (uint)arrayLength)
{
throw new IndexOutOfRangeException();
}

JsonRow row = _parsedData.Get(currentIndex);
if (!row.NeedUnescaping)
{
return new JwtElement(this, currentIndex + ((arrayIndex + 1) * JsonRow.Size));
return new JwtElement(this, arrayIndex * JsonRow.Size);
}

int elementCount = 0;
Expand Down Expand Up @@ -631,7 +613,6 @@ internal JwtElement GetArrayIndexElement(int currentIndex, int arrayIndex)
throw new IndexOutOfRangeException();
}


/// <summary>Determines whether the <see cref="JwtDocument"/> contains the specified key.</summary>
public bool ContainsKey(ReadOnlySpan<byte> key)
=> _root.TryGetProperty(key, out _);
Expand Down
29 changes: 25 additions & 4 deletions src/JsonWebToken/Reader/JwtElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public readonly struct JwtElement
{
private readonly JwtDocument _parent;
private readonly int _idx;
private readonly JwtDocument? _document;

internal JwtElement(JwtDocument parent, int idx)
{
Expand All @@ -23,8 +24,28 @@ internal JwtElement(JwtDocument parent, int idx)
Debug.Assert(idx >= 0);
Debug.Assert(parent != null);

_parent = parent;
_idx = idx;

if (parent.GetJsonTokenType(idx) is JsonTokenType.StartArray)
{
var value = parent.GetRawValue(idx);
int count = parent.GetArrayLength(idx);
if (!ArrayEnumerator.TryParse(value, count, out var doc))
{
ThrowHelper.ThrowFormatException_MalformedJson();
_document = default;
}
else
{
_document = doc;
}
}
else
{
_document = default;
}

_parent = parent;
}

/// <summary>Defines whether the current <see cref="JwtElement"/> is empty</summary>
Expand All @@ -47,9 +68,9 @@ public JwtElement this[int index]
{
get
{
if (_parent != null)
if (_document != null)
{
return _parent.GetArrayIndexElement(_idx, index);
return _document.GetArrayIndexElement(0, index);
}

throw new IndexOutOfRangeException();
Expand Down Expand Up @@ -884,7 +905,7 @@ public bool MoveNext()
return _curIdx < _endIdxOrVersion;
}

private static bool TryParse(ReadOnlyMemory<byte> utf8Array, int count, out JwtDocument? document)
internal static bool TryParse(ReadOnlyMemory<byte> utf8Array, int count, [NotNullWhen(true)] out JwtDocument? document)
{
ReadOnlySpan<byte> utf8JsonSpan = utf8Array.Span;
var database = new JsonMetadata(count * JsonRow.Size);
Expand Down
2 changes: 1 addition & 1 deletion src/JsonWebToken/Reader/JwtHeaderDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace JsonWebToken
// Based on https://github.com/dotnet/runtime/blob/master/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs
public sealed class JwtHeaderDocument : IDisposable
{
internal static readonly JwtHeaderDocument Empty = new JwtHeaderDocument(new JwtDocument(), -1, -1, -1, -1);
internal static readonly JwtHeaderDocument Empty = new JwtHeaderDocument(JwtDocument.Empty, -1, -1, -1, -1);

private readonly JwtDocument _document;
private readonly JwtElement _alg;
Expand Down
2 changes: 1 addition & 1 deletion src/JsonWebToken/Reader/JwtPayloadDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace JsonWebToken
// Based on https://github.com/dotnet/runtime/blob/master/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs
public sealed class JwtPayloadDocument : IDisposable
{
internal static readonly JwtPayloadDocument Empty = new JwtPayloadDocument(new JwtDocument(), 0, -1);
internal static readonly JwtPayloadDocument Empty = new JwtPayloadDocument(JwtDocument.Empty, 0, -1);

private readonly JwtDocument _document;
private readonly byte _control;
Expand Down
19 changes: 12 additions & 7 deletions test/JsonWebToken.Tests/JwksTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ public void ReadJwks_Invalid(string issuer, string json)
Assert.ThrowsAny<Exception>(() => Jwks.FromJson(issuer, json));
}

private readonly object _lock = new object();

[Fact]
public void OnJwksRefreshed()
{
Expand All @@ -178,14 +180,17 @@ public void OnJwksRefreshed()
key384,
key512
};
Jwks.OnJwksRefreshed += Jwks_OnJwksRefreshed;
try
{
Jwks.PublishJwksRefreshed(oldJwks, newJwks);
}
finally
lock (_lock)
{
Jwks.OnJwksRefreshed -= Jwks_OnJwksRefreshed;
Jwks.OnJwksRefreshed += Jwks_OnJwksRefreshed;
try
{
Jwks.PublishJwksRefreshed(oldJwks, newJwks);
}
finally
{
Jwks.OnJwksRefreshed -= Jwks_OnJwksRefreshed;
}
}
}

Expand Down
16 changes: 14 additions & 2 deletions test/JsonWebToken.Tests/JwtDocumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ public void Issue489_NoValidation_Valid(string token)
[Fact]
public void JwtHeaderDocument_TryParse()
{
var json = Encoding.UTF8.GetBytes("{\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"]}");
var json = Encoding.UTF8.GetBytes("{\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"],\"€$cap€d\":true,\"Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d\":true}");

var result = JwtHeaderDocument.TryParseHeader(json, null, TokenValidationPolicy.NoValidation, out var header, out var error);
Assert.True(result);
Expand All @@ -312,13 +312,16 @@ public void JwtHeaderDocument_TryParse()
Assert.Equal(1, GetProperty(header, "object").GetJsonDocument().RootElement.GetProperty("value").GetInt64());
Assert.NotNull(GetProperty(header, "array").GetJsonDocument().RootElement.EnumerateArray().ToArray());
Assert.Equal(JsonValueKind.Null, GetProperty(header, "null").ValueKind);
Assert.Equal(JsonValueKind.True, GetProperty(header, "€$cap€d").ValueKind);
Assert.Equal(JsonValueKind.True, GetProperty(header, "Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d").ValueKind);
Assert.Equal(default, GetProperty(header, "unknonw"));
Assert.Null(error);
}

[Fact]
public void JwtPayloadDocument_TryParse()
{
var json = Encoding.UTF8.GetBytes("{\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"]}");
var json = Encoding.UTF8.GetBytes("{\"Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d\":true,\"string\":\"hello\",\"number\":1234,\"boolean\":true,\"object\":{\"value\":1},\"null\":null,\"array\":[\"hello\",\"world\"],\"€$cap€d\":true,\"escaped_value\":\"€$cap€d value\"}");

var result = JwtPayloadDocument.TryParsePayload(json, null, TokenValidationPolicy.NoValidation, out var payload, out var error);
Assert.True(result);
Expand All @@ -328,7 +331,16 @@ public void JwtPayloadDocument_TryParse()
Assert.Equal(1, GetProperty(payload, "object").GetJsonDocument().RootElement.GetProperty("value").GetInt64());
Assert.NotNull(GetProperty(payload, "array").GetJsonDocument().RootElement.EnumerateArray().ToArray());
Assert.Equal(JsonValueKind.Null, GetProperty(payload, "null").ValueKind);
Assert.Equal(JsonValueKind.True, GetProperty(payload, "€$cap€d").ValueKind);
Assert.Equal(JsonValueKind.True, GetProperty(payload, "Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong €$cap€d").ValueKind);
Assert.Equal(default, GetProperty(payload, "unknown"));
Assert.Null(error);

var rawText = GetProperty(payload, "null").GetPropertyRawText();
Assert.Equal("\"null\":null", rawText);

var escapedValue = GetProperty(payload, "escaped_value");
Assert.True(escapedValue.ValueEquals("€$cap€d value"));
}

private static JwtElement GetProperty(JwtHeaderDocument document, string name)
Expand Down
Loading