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,3 +1,7 @@
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, string claimName) -> object
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,8 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan<byte> byteSpan)
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
ReadPayloadValue(ref reader, claims);
string claimName = reader.GetString();
claims[claimName] = ReadTokenPayloadValueDelegate(ref reader, claimName);
}
// 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,72 +52,63 @@ 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="claimName">The claim at the current position of the reader.</param>
/// <returns>A claim that was read.</returns>
internal static object ReadTokenPayloadValue(ref Utf8JsonReader reader, string claimName)
{
if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud))
{
_audiences = [];
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;
return _audiences[0];
}
}
return _audiences;
}
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp))
{
_azp = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Azp, ClassName, true);
claims[JwtRegisteredClaimNames.Azp] = _azp;
return 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;
return 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;
return 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;
return 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;
return 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;
return 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;
return JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true);
}
else
{
string propertyName = reader.GetString();
claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true);
return JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, claimName, JsonClaimSet.ClassName, true);
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ public JsonWebToken(string jwtEncodedString)
_encodedToken = jwtEncodedString;
}

/// <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>
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 <see cref="JsonWebToken"/> from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format.
/// </summary>
Expand Down Expand Up @@ -141,6 +160,24 @@ public JsonWebToken(string header, string payload)
_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
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 @@
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="claimName">The name of the claim being read.</param>
/// <returns></returns>
internal delegate object ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader, string claimName);

Check failure on line 222 in src/Microsoft.IdentityModel.Tokens/Delegates.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Symbol 'ReadTokenPayloadValueDelegate' is not part of the declared API (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 222 in src/Microsoft.IdentityModel.Tokens/Delegates.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Symbol 'ReadTokenPayloadValueDelegate' is not part of the declared API (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 222 in src/Microsoft.IdentityModel.Tokens/Delegates.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Symbol 'ReadTokenPayloadValueDelegate' is not part of the declared API (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 222 in src/Microsoft.IdentityModel.Tokens/Delegates.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Symbol 'ReadTokenPayloadValueDelegate' is not part of the declared API (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 222 in src/Microsoft.IdentityModel.Tokens/Delegates.cs

View workflow job for this annotation

GitHub Actions / Build and run unit tests

Symbol 'ReadTokenPayloadValueDelegate' is not part of the declared API (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Microsoft.IdentityModel.Tokens.TokenReplayValidationError.TokenReplayValidationE
Microsoft.IdentityModel.Tokens.TokenTypeValidationError
Microsoft.IdentityModel.Tokens.TokenTypeValidationError.InvalidTokenType.get -> string
Microsoft.IdentityModel.Tokens.TokenTypeValidationError.TokenTypeValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidTokenType, System.Exception innerException = null) -> void
Microsoft.IdentityModel.Tokens.TokenValidationParameters.ReadTokenPayloadValue.get -> Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate
Microsoft.IdentityModel.Tokens.TokenValidationParameters.ReadTokenPayloadValue.set -> void
Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.get -> System.TimeProvider
Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.set -> void
Microsoft.IdentityModel.Tokens.ValidatedToken.Log(Microsoft.Extensions.Logging.ILogger logger) -> void
Expand Down
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.

Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
using System.Reflection;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
using Microsoft.IdentityModel.TestUtils;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Tokens.Json;
using Microsoft.IdentityModel.Tokens.Json.Tests;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -1741,35 +1743,59 @@ public void StringAndMemoryConstructors_CreateEquivalentTokens(JwtTheoryData the
}

[Fact]
public void DerivedJsonWebToken_IsCreatedCorrectly()
public void CreateTokenWithoutKeyIdentifiersInHeader()
{
var expectedCustomClaim = "customclaim";
var tokenStr = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
string rawToken = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
{
SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials,
IncludeKeyIdInHeader = false
});
var token = new JsonWebToken(rawToken);
Assert.False(token.TryGetHeaderValue(JwtHeaderParameterNames.Kid, out string _));
Assert.False(token.TryGetHeaderValue(JwtHeaderParameterNames.X5t, out string _));
}

[Fact]
public void ReadTokenDelegates_CalledCorrectly()
{
var tokenSpan = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Issuer = Default.Issuer,
Claims = new Dictionary<string, object>
{
{ "CustomClaim", expectedCustomClaim },
{ "CustomPayload", "custom_payload" },
},
AdditionalHeaderClaims = new Dictionary<string, object>
{
{ "CustomHeader", "custom_header" }
}
});
}).AsMemory();

object ReadPayloadValue(ref Utf8JsonReader reader, string claimName)
{
if (reader.ValueTextEquals("CustomPayload"u8))
{
return new CustomPayloadClaim(JsonSerializerPrimitives.ReadString(ref reader, "CustomPayload", string.Empty, true));
}
return JsonWebToken.ReadTokenPayloadValue(ref reader, claimName);
}

var jwt = new JsonWebToken(tokenSpan, ReadPayloadValue);

var derivedToken = new CustomJsonWebToken(tokenStr);
Assert.True(jwt.TryGetHeaderValue<string>("CustomHeader", out var actualHeaderClaim));
Assert.True(jwt.TryGetPayloadValue<CustomPayloadClaim>("CustomPayload", out var actualPayloadClaim));

Assert.Equal(expectedCustomClaim, derivedToken.CustomClaim);
Assert.Equal(Default.Issuer, derivedToken.Issuer);
Assert.Equal("custom_header", actualHeaderClaim);
Assert.Equal("custom_payload", actualPayloadClaim.CustomValue);
}

[Fact]
public void CreateTokenWithoutKeyIdentifiersInHeader()
private class CustomHeaderClaim(string customValue)
{
string rawToken = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
{
SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials,
IncludeKeyIdInHeader = false
});
var token = new JsonWebToken(rawToken);
Assert.False(token.TryGetHeaderValue(JwtHeaderParameterNames.Kid, out string _));
Assert.False(token.TryGetHeaderValue(JwtHeaderParameterNames.X5t, out string _));
public string CustomValue { get; set; } = customValue;
}
private class CustomPayloadClaim(string customValue)
{
public string CustomValue { get; set; } = customValue;
}
}

Expand Down
Loading