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

Replace JsonWebToken ReadPayloadValue with a delegate #2981

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string header, string payload, System.Collections.Generic.IDictionary<string, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate> readTokenPayloadValueDelegates) -> void
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string jwtEncodedString, System.Collections.Generic.IDictionary<string, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate> readTokenPayloadValueDelegates) -> void
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(System.ReadOnlyMemory<char> encodedTokenMemory, System.Collections.Generic.IDictionary<string, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate> readTokenPayloadValueDelegates) -> void
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ReadTokenPayloadValueDelegates.get -> System.Collections.Generic.IDictionary<string, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate>
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ReadTokenPayloadValueDelegates.set -> void
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler._telemetryClient -> Microsoft.IdentityModel.Telemetry.ITelemetryClient
override Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
override Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateTokenAsync(string token, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal class JsonClaimSet

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

internal JsonClaimSet(Dictionary<string, object> jsonClaims)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Tokens.Json;

namespace Microsoft.IdentityModel.JsonWebTokens
Expand Down Expand Up @@ -40,7 +39,71 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan<byte> byteSpan)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
ReadPayloadValue(ref reader, claims);
if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud))
{
List<string> _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))
Copy link
Member

@brentschmaltz brentschmaltz Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user will have no ability to parse any of the claims that are parsed before calling the delegate.

There are a couple of things we can do.

  • Have a model where a power user can take total control.

  • Call the delegate if hook has been added for 'aud' or any of the internal types.
    in this case we could create a u8 byte array for the claim type associated with a delegate so that reader.ValueTextEquals performant check could be used.

{
claims[JwtRegisteredClaimNames.Azp] = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Exp))
{
claims[JwtRegisteredClaimNames.Exp] = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iat))
{
claims[JwtRegisteredClaimNames.Iat] = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss))
{
claims[JwtRegisteredClaimNames.Iss] = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti))
{
claims[JwtRegisteredClaimNames.Jti] = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Nbf))
{
claims[JwtRegisteredClaimNames.Nbf] = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Sub))
{
claims[JwtRegisteredClaimNames.Sub] = JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true);
}
else
{
string propertyName = reader.GetString();

if (ReadTokenPayloadValueDelegates.TryGetValue(propertyName, out var readTokenPayloadValueDelegate) && readTokenPayloadValueDelegate != null)
{
reader.Read();
claims[propertyName] = readTokenPayloadValueDelegate(ref reader);
reader.Read();
}
else
{
claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true);
}
}
}
// We read a JsonTokenType.StartObject above, exiting and positioning reader at next token.
else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false))
Expand All @@ -51,74 +114,5 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan<byte> byteSpan)

return new JsonClaimSet(claims);
}

private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary<string, object> claims)
{
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))
{
_azp = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true);
claims[JwtRegisteredClaimNames.Azp] = _azp;
}
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))
{
_iss = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true);
claims[JwtRegisteredClaimNames.Iss] = _iss;
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti))
{
_jti = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true);
claims[JwtRegisteredClaimNames.Jti] = _jti;
}
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);
}
}
}
}
103 changes: 103 additions & 0 deletions src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,33 @@ public JsonWebToken(string jwtEncodedString)
_encodedToken = jwtEncodedString;
}

/// <summary>
/// Initializes a new instance of <see cref="JsonWebToken"/> from a string in JWS or JWE Compact serialized format.
/// </summary>
/// <param name="jwtEncodedString">A JSON Web Token that has been serialized in JWS or JWE Compact serialized format.</param>
/// <param name="readTokenPayloadValueDelegates">Custom delegates to be called when reading an associated claim names.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="jwtEncodedString"/> is null or empty.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="jwtEncodedString"/> is not in JWS or JWE Compact Serialization format.</exception>
/// <remarks>
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7519"/> (JWT).
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7515"/> (JWS).
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7516"/> (JWE).
/// <para>
/// The contents of the returned <see cref="JsonWebToken"/> have not been validated, the JSON Web Token is simply decoded. Validation can be accomplished using the validation methods in <see cref="JsonWebTokenHandler"/>
/// </para>
/// </remarks>
internal JsonWebToken(string jwtEncodedString, IDictionary<string, ReadTokenPayloadValueDelegate> readTokenPayloadValueDelegates)
{
if (string.IsNullOrEmpty(jwtEncodedString))
throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(jwtEncodedString)));

ReadTokenPayloadValueDelegates = readTokenPayloadValueDelegates ?? new Dictionary<string, ReadTokenPayloadValueDelegate>();

ReadToken(jwtEncodedString.AsMemory());

_encodedToken = jwtEncodedString;
}

/// <summary>
/// Initializes a new instance of <see cref="JsonWebToken"/> from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format.
/// </summary>
Expand All @@ -107,6 +134,33 @@ public JsonWebToken(ReadOnlyMemory<char> encodedTokenMemory)
_encodedTokenMemory = encodedTokenMemory;
}

/// <summary>
/// Initializes a new instance of <see cref="JsonWebToken"/> from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format.
/// </summary>
/// <param name="encodedTokenMemory">A ReadOnlyMemory{char} containing the JSON Web Token serialized in JWS or JWE Compact format.</param>
/// <param name="readTokenPayloadValueDelegates">Custom delegates to be called when reading an associated claim names.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="encodedTokenMemory"/> is empty.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="encodedTokenMemory"/> does not represent a valid JWS or JWE Compact Serialization format.</exception>
/// <remarks>
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7519"/> (JWT).
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7515"/> (JWS).
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7516"/> (JWE).
/// <para>
/// The contents of the returned <see cref="JsonWebToken"/> have not been validated; the JSON Web Token is simply decoded. Validation can be performed using the methods in <see cref="JsonWebTokenHandler"/>.
/// </para>
/// </remarks>
internal JsonWebToken(ReadOnlyMemory<char> encodedTokenMemory, IDictionary<string, ReadTokenPayloadValueDelegate> readTokenPayloadValueDelegates)
{
if (encodedTokenMemory.IsEmpty)
throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(encodedTokenMemory)));

ReadTokenPayloadValueDelegates = readTokenPayloadValueDelegates ?? new Dictionary<string, ReadTokenPayloadValueDelegate>();

ReadToken(encodedTokenMemory);

_encodedTokenMemory = encodedTokenMemory;
}

/// <summary>
/// Initializes a new instance of the <see cref="JsonWebToken"/> class where the header contains the crypto algorithms applied to the encoded header and payload.
/// </summary>
Expand Down Expand Up @@ -138,6 +192,55 @@ public JsonWebToken(string header, string payload)
_encodedToken = encodedToken;
}

/// <summary>
/// Initializes a new instance of the <see cref="JsonWebToken"/> class where the header contains the crypto algorithms applied to the encoded header and payload.
/// </summary>
/// <param name="header">A string containing JSON which represents the cryptographic operations applied to the JWT and optionally any additional properties of the JWT.</param>
/// <param name="payload">A string containing JSON which represents the claims contained in the JWT. Each claim is a JSON object of the form { Name, Value }. Can be the empty.</param>
/// <param name="readTokenPayloadValueDelegates">Custom delegates to be called when reading an associated claim names.</param>
/// <remarks>
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7519"/> (JWT).
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7515"/> (JWS).
/// See: <see href="https://datatracker.ietf.org/doc/html/rfc7516"/> (JWE).
/// <para>
/// The contents of the returned <see cref="JsonWebToken"/> have not been validated, the JSON Web Token is simply decoded. Validation can be accomplished using the validation methods in <see cref="JsonWebTokenHandler"/>
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="header"/> is null or empty.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="payload"/> is null.</exception>
internal JsonWebToken(string header, string payload, IDictionary<string, ReadTokenPayloadValueDelegate> readTokenPayloadValueDelegates)
{
if (string.IsNullOrEmpty(header))
throw LogHelper.LogArgumentNullException(nameof(header));

_ = payload ?? throw LogHelper.LogArgumentNullException(nameof(payload));

var encodedHeader = Base64UrlEncoder.Encode(header);
var encodedPayload = Base64UrlEncoder.Encode(payload);
var encodedToken = encodedHeader + "." + encodedPayload + ".";

ReadTokenPayloadValueDelegates = readTokenPayloadValueDelegates ?? new Dictionary<string, ReadTokenPayloadValueDelegate>();

ReadToken(encodedToken.AsMemory());

_encodedToken = encodedToken;
}

/// <summary>
/// Gets or sets the <see cref="IDictionary{String, Object}"/> that contains a collection of claim name and delegate pairs.
/// When reading token payload claims, a delegate will be called if the token has an associated claim.
/// </summary>
/// <remarks>
/// An example implementation:
/// <code>
/// object ReadPayloadValueDelegate(ref Utf8JsonReader reader) =&gt;
/// {
/// return JsonSerializer.Deserialize&lt;CustomPayloadClaim&gt;(reader.GetString());
/// }
/// </code>
/// </remarks>
internal IDictionary<string, ReadTokenPayloadValueDelegate> ReadTokenPayloadValueDelegates { get; set; } = new Dictionary<string, ReadTokenPayloadValueDelegate>();

internal string ActualIssuer { get; set; }

internal ClaimsIdentity ActorClaimsIdentity { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara
#pragma warning disable CA1031 // Do not catch general exception types
try
{
jsonWebToken = new JsonWebToken(token);
jsonWebToken = new JsonWebToken(token, validationParameters.ReadTokenPayloadValueDelegates);
}
catch (Exception ex)
{
Expand Down
10 changes: 10 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/Delegates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;

namespace Microsoft.IdentityModel.Tokens
Expand Down Expand Up @@ -210,4 +211,13 @@ internal delegate ValidationResult<SecurityKey> SignatureValidationDelegate(
BaseConfiguration? configuration,
CallContext callContext);
#nullable restore

/// <summary>
/// When token payload is being read claim by claim,
/// this delegate is called after all claims known to the library have been processed.
/// When called, the reader is positioned at the claim value.
/// </summary>
/// <param name="reader">Reader for the underlying token bytes.</param>
/// <returns>Claim value associated with the claim name.</returns>
internal delegate object ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader);
}
Loading
Loading