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

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
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,6 +1,12 @@
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>>
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string header, string payload, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) -> void
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string jwtEncodedString, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) -> void
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(System.ReadOnlyMemory<char> encodedTokenMemory, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) -> void
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ReadTokenPayloadValueDelegate.get -> Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate
Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ReadTokenPayloadValueDelegate.set -> void
static Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ReadTokenPayloadValue(ref System.Text.Json.Utf8JsonReader reader, System.Collections.Generic.IDictionary<string, object> claims) -> void
static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateToken(string payload, Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor tokenDescriptor) -> string
static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.EncryptToken(byte[] innerTokenUtf8Bytes, Microsoft.IdentityModel.Tokens.EncryptingCredentials encryptingCredentials, string compressionAlgorithm, System.Collections.Generic.IDictionary<string, object> additionalHeaderClaims, string tokenType, bool includeKeyIdInHeader) -> string
static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.EncryptToken(byte[] innerTokenUtf8Bytes, Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor tokenDescriptor) -> string
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,7 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan<byte> byteSpan)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
ReadPayloadValue(ref reader, claims);
ReadTokenPayloadValueDelegate(ref reader, claims);
}
// We read a JsonTokenType.StartObject above, exiting and positioning reader at next token.
else if (JsonSerializerPrimitives.IsReaderAtTokenType(ref reader, JsonTokenType.EndObject, false))
Expand All @@ -52,11 +51,17 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan<byte> byteSpan)
return new JsonClaimSet(claims);
}

private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary<string, object> claims)
/// <summary>
/// Reads and saves the value of the payload claim from the reader.
/// </summary>
/// <param name="reader">The reader over the JWT.</param>
/// <param name="claims">A collection to hold claims that have been read.</param>
/// <returns>A claim that was read.</returns>
internal static void ReadTokenPayloadValue(ref Utf8JsonReader reader, IDictionary<string, object> claims)
{
if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud))
{
_audiences = [];
List<string> _audiences = [];
reader.Read();
if (reader.TokenType == JsonTokenType.StartArray)
{
Expand All @@ -78,41 +83,31 @@ private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDict
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp))
{
_azp = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true);
claims[JwtRegisteredClaimNames.Azp] = _azp;
claims[JwtRegisteredClaimNames.Azp] = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Exp))
{
_exp = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true);
_expDateTime = EpochTime.DateTime(_exp.Value);
claims[JwtRegisteredClaimNames.Exp] = _exp;
claims[JwtRegisteredClaimNames.Exp] = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Exp, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iat))
{
_iat = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true);
_iatDateTime = EpochTime.DateTime(_iat.Value);
claims[JwtRegisteredClaimNames.Iat] = _iat;
claims[JwtRegisteredClaimNames.Iat] = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Iat, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Iss))
{
_iss = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true);
claims[JwtRegisteredClaimNames.Iss] = _iss;
claims[JwtRegisteredClaimNames.Iss] = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Iss, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Jti))
{
_jti = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true);
claims[JwtRegisteredClaimNames.Jti] = _jti;
claims[JwtRegisteredClaimNames.Jti] = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Jti, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Nbf))
{
_nbf = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true);
_nbfDateTime = EpochTime.DateTime(_nbf.Value);
claims[JwtRegisteredClaimNames.Nbf] = _nbf;
claims[JwtRegisteredClaimNames.Nbf] = JsonSerializerPrimitives.ReadLong(ref reader, JwtRegisteredClaimNames.Nbf, ClassName, true);
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Sub))
{
_sub = JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true);
claims[JwtRegisteredClaimNames.Sub] = _sub;
claims[JwtRegisteredClaimNames.Sub] = JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true);
}
else
{
Expand Down
106 changes: 106 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="readTokenPayloadValueDelegate">A custom delegate to be called when each payload claim is being read. If null, default implementation is called.</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, ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate)
{
if (string.IsNullOrEmpty(jwtEncodedString))
throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(jwtEncodedString)));

ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue;

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="readTokenPayloadValueDelegate">A custom delegate to be called when each payload claim is being read. If null, default implementation is called.</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, ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate)
{
if (encodedTokenMemory.IsEmpty)
throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(encodedTokenMemory)));

ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue;

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,58 @@ 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="readTokenPayloadValueDelegate">A custom delegate to be called when each payload claim is being read. If null, default implementation is called.</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, ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate)
{
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 + ".";

ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue;

ReadToken(encodedToken.AsMemory());

_encodedToken = encodedToken;
}

/// <summary>
/// Called for each claim when token payload is being read.
/// </summary>
/// <remarks>
/// An example implementation:
/// <code>
/// object ReadPayloadValueDelegate(ref Utf8JsonReader reader, string claimName) =>
/// {
/// if (reader.ValueTextEquals("CustomProp"))
/// {
/// return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true);
/// }
/// return JsonWebToken.ReadTokenPayloadValue(ref reader, claimName);
/// }
/// </code>
/// </remarks>
internal ReadTokenPayloadValueDelegate ReadTokenPayloadValueDelegate { get; set; } = ReadTokenPayloadValue;

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.ReadTokenPayloadValue);
}
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>
/// Definition for ReadTokenPayloadValueDelegate.
/// Called for each claim when token payload is being read.
/// </summary>
/// <param name="reader">Reader for the underlying token bytes.</param>
/// <param name="claims">A collection to hold claims that have been read.</param>
/// <returns></returns>
internal delegate void ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader, IDictionary<string, object> claims);
}
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,11 @@ public string NameClaimType
/// </summary>
public IDictionary<string, object> PropertyBag { get; set; }

/// <summary>
/// Gets or sets a delegate that will be called when reading token payload claims.
/// </summary>
internal ReadTokenPayloadValueDelegate ReadTokenPayloadValue { get; set; }

/// <summary>
/// Gets or sets a boolean to control if configuration required to be refreshed before token validation.
/// </summary>
Expand Down

This file was deleted.

Loading
Loading