Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

In JsonWebToken use the values as bytes instead of converting to string #2535

Draft
wants to merge 36 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bfb1fed
Update JsonWebToken for child classes.
pmaytak Mar 21, 2024
4eb7a5f
Add props files to solution.
pmaytak Mar 21, 2024
76d9e52
Fix.
pmaytak Mar 27, 2024
b08ffa1
Add another claims dictionary to JsonWebToken.Payload to hold string …
pmaytak Apr 4, 2024
22720d7
Using and reading from the original token as a Span and saving claim …
pmaytak Apr 4, 2024
e4ab2c1
Merge main.
pmaytak Apr 20, 2024
e597160
Rename.
pmaytak Apr 24, 2024
63ab5cd
Remove ArrayPool from payload claim set.
pmaytak Apr 24, 2024
bc3140c
Rename.
pmaytak Apr 24, 2024
8397e34
Add ReadUtf8StringLocation
pmaytak Apr 25, 2024
2980f41
Make private protected.
pmaytak May 4, 2024
a14cf2c
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak May 4, 2024
a4f1728
Merge origin/dev.
pmaytak Jun 12, 2024
eb4b024
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak Jun 14, 2024
fb3639d
Refactor.
pmaytak Jun 18, 2024
7e4f21e
Add test.
pmaytak Jun 18, 2024
edbe8f2
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak Jun 18, 2024
464b592
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak Jun 18, 2024
7b968be
Fixes to claims.
pmaytak Jun 19, 2024
aac0539
Update reading string header properties.
pmaytak Jun 20, 2024
85a384f
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak Jun 20, 2024
652b333
Add delegate for reading properties instead of an overload.
pmaytak Jul 31, 2024
a7f36fb
Add delegate for reading properties instead of an overload.
pmaytak Aug 3, 2024
3f6eaa0
Merge main.
pmaytak Aug 3, 2024
c86355e
Add test. Update comments.
pmaytak Aug 7, 2024
cbedbca
Merge dev.
pmaytak Aug 7, 2024
f22e584
Add code to unscape values.
pmaytak Aug 8, 2024
1dbd7cf
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak Aug 14, 2024
5b8c869
Fixes.
pmaytak Aug 15, 2024
1e73609
Update reading escaped values. Update test.
pmaytak Aug 23, 2024
9d990f2
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak Aug 23, 2024
57d9b46
Merge remote-tracking branch 'origin/dev' into pmaytak/1pjwt
pmaytak Aug 29, 2024
f013da8
Add benchmark to test validation with an issuer delegate using string…
pmaytak Sep 4, 2024
c9d257f
Add a class with profiler methods.
pmaytak Sep 7, 2024
75ebb5a
Default TVP IssuerBytes to an empty Span.
pmaytak Sep 13, 2024
ba4b49f
Revert to use ArrayPool for the token bytes. Save bytes only for stri…
pmaytak Sep 14, 2024
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
97 changes: 90 additions & 7 deletions src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.ObjectModel;
using System.Globalization;
using System.Security.Claims;
using System.Text;
pmaytak marked this conversation as resolved.
Show resolved Hide resolved
using System.Text.Json;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
Expand All @@ -24,14 +25,44 @@ internal class JsonClaimSet

internal object _claimsLock = new();
internal readonly Dictionary<string, object> _jsonClaims;

#if NET8_0_OR_GREATER
brentschmaltz marked this conversation as resolved.
Show resolved Hide resolved
internal readonly Dictionary<string, (int startIndex, int length)?> _jsonClaimsBytes;
internal readonly Memory<byte> _tokenAsMemory;
pmaytak marked this conversation as resolved.
Show resolved Hide resolved
#endif

private List<Claim> _claims;

internal JsonClaimSet() { _jsonClaims = new Dictionary<string, object>(); }
internal JsonClaimSet()
{
_jsonClaims = [];

#if NET8_0_OR_GREATER
_jsonClaimsBytes = [];
_tokenAsMemory = Memory<byte>.Empty;
#endif
}

internal JsonClaimSet(Dictionary<string, object> jsonClaims)
{
_jsonClaims = jsonClaims;

#if NET8_0_OR_GREATER
_jsonClaimsBytes = [];
#endif
}

#if NET8_0_OR_GREATER
internal JsonClaimSet(
Dictionary<string, object> jsonClaims,
Dictionary<string, (int startIndex, int length)?> jsonClaimsBytes,
Memory<byte> tokenAsMemory)
{
_jsonClaims = jsonClaims;
_jsonClaimsBytes = jsonClaimsBytes;
_tokenAsMemory = tokenAsMemory;
}
#endif
pmaytak marked this conversation as resolved.
Show resolved Hide resolved

internal List<Claim> Claims(string issuer)
{
Expand All @@ -48,6 +79,14 @@ internal List<Claim> CreateClaims(string issuer)
foreach (KeyValuePair<string, object> 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<string, (int StartIndex, int Length)?> 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;
}

Expand All @@ -71,7 +110,7 @@ internal static void CreateClaimFromObject(List<Claim> 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)
Expand Down Expand Up @@ -147,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)
Expand All @@ -159,17 +202,42 @@ 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;

return obj.ToString();
}

#endif
return string.Empty;
}

#if NET8_0_OR_GREATER
// Similar to GetStringValue but returns the bytes directly.
internal ReadOnlySpan<byte> GetStringBytesValue(string key)
{
if (_jsonClaimsBytes.TryGetValue(key, out (int StartIndex, int Length)? location))
{
if (!location.HasValue)
return null;

return _tokenAsMemory.Slice(location.Value.StartIndex, location.Value.Length).Span;
}

return new Span<byte>();
pmaytak marked this conversation as resolved.
Show resolved Hide resolved
}
#endif

internal DateTime GetDateTime(string key)
{
long l = GetValue<long>(key, false, out bool found);
Expand Down Expand Up @@ -317,7 +385,7 @@ internal T GetValue<T>(string key, bool throwEx, out bool found)
else if (typeof(T) == typeof(Collection<object>))
return (T)(object)new Collection<object> { obj };

else if(typeof(T).IsEnum)
else if (typeof(T).IsEnum)
{
return (T)Enum.Parse(typeof(T), obj.ToString(), ignoreCase: true);
}
Expand All @@ -339,15 +407,15 @@ internal T GetValue<T>(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))
return (T)(object)new long[] { value };
}
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))
Expand Down Expand Up @@ -422,13 +490,28 @@ internal bool TryGetClaim(string key, string issuer, out Claim claim)
/// <returns></returns>
internal bool TryGetValue<T>(string key, out T value)
{
#if NET8_0_OR_GREATER
if (typeof(T) == typeof(string))
{
var span = GetStringBytesValue(key);
if (!span.IsEmpty)
{
value = (T)(object)Encoding.UTF8.GetString(span);
return true;
}
}
#endif
value = GetValue<T>(key, false, out bool found);
return found;
}

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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
{
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<byte> byteSpan)
{
Utf8JsonReader reader = new(byteSpan);
internal JsonClaimSet CreateHeaderClaimSet(Memory<byte> tokenHeaderAsMemory)
pmaytak marked this conversation as resolved.
Show resolved Hide resolved
{
Utf8JsonReader reader = new(tokenHeaderAsMemory.Span);
if (!JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.StartObject, true))
throw LogHelper.LogExceptionMessage(
new JsonException(
Expand All @@ -36,46 +36,19 @@
LogHelper.MarkAsNonPII(reader.CurrentDepth),
LogHelper.MarkAsNonPII(reader.BytesConsumed))));

Dictionary<string, object> claims = new();
Dictionary<string, object> claims = [];
#if NET8_0_OR_GREATER
Dictionary<string, (int startIndex, int length)?> 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))
Expand All @@ -84,7 +57,94 @@
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<string, object> 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<string, object> claims,
Dictionary<string, (int startIndex, int length)?> claimsBytes,
Memory<byte> 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);

Check failure on line 120 in src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs

View workflow job for this annotation

GitHub Actions / Wilson GitHub Action Test

'JwtRegisteredClaimNames' does not contain a definition for 'Alg'

Check failure on line 120 in src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.HeaderClaimSet.cs

View workflow job for this annotation

GitHub Actions / Wilson GitHub Action Test

'JwtRegisteredClaimNames' does not contain a definition for 'Alg'
}
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
}
}
Loading
Loading