From 4d5347b55a82c7b4aff207b2b721d9c2266252f0 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:18:55 -0800 Subject: [PATCH] Replace overload with delegates to read token values. --- .../InternalAPI.Unshipped.txt | 4 ++ .../Json/JsonClaimSet.cs | 2 +- .../Json/JsonWebToken.PayloadClaimSet.cs | 49 ++++++--------- .../JsonWebToken.cs | 37 +++++++++++ .../Delegates.cs | 10 +++ .../InternalAPI.Unshipped.txt | 2 + .../TokenValidationParameters.cs | 5 ++ .../CustomJsonWebToken.cs | 42 ------------- .../JsonWebTokenTests.cs | 62 +++++++++++++------ 9 files changed, 123 insertions(+), 90 deletions(-) delete mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt index cb23d777b2..16fa6559da 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt @@ -1,3 +1,7 @@ +Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(System.ReadOnlyMemory 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 additionalHeaderClaims, string tokenType, bool includeKeyIdInHeader) -> string static Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.EncryptToken(byte[] innerTokenUtf8Bytes, Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor tokenDescriptor) -> string diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs index 7bcfb4d4ce..7ad045fc9c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs @@ -28,7 +28,7 @@ internal class JsonClaimSet internal JsonClaimSet() { - _jsonClaims = new Dictionary(); + _jsonClaims = []; } internal JsonClaimSet(Dictionary jsonClaims) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index dac70e195b..e838282842 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -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 @@ -40,7 +39,8 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan 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)) @@ -52,72 +52,63 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) return new JsonClaimSet(claims); } - private protected virtual void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) + /// + /// Reads and saves the value of the payload claim from the reader. + /// + /// The reader over the JWT. + /// The claim at the current position of the reader. + /// A claim that was read. + internal static object ReadTokenPayloadValue(ref Utf8JsonReader reader, string claimName) { if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { - _audiences = []; + List _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); } } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 48af759d86..4727578cfe 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -86,6 +86,25 @@ public JsonWebToken(string jwtEncodedString) _encodedToken = jwtEncodedString; } + /// + /// Initializes a new instance of from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format. + /// + /// A ReadOnlyMemory{char} containing the JSON Web Token serialized in JWS or JWE Compact format. + /// A custom delegate to be called when each payload claim is being read. If null, default implementation is called. + internal JsonWebToken( + ReadOnlyMemory encodedTokenMemory, + ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) + { + if (encodedTokenMemory.IsEmpty) + throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(encodedTokenMemory))); + + ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue; + + ReadToken(encodedTokenMemory); + + _encodedTokenMemory = encodedTokenMemory; + } + /// /// Initializes a new instance of from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format. /// @@ -141,6 +160,24 @@ public JsonWebToken(string header, string payload) _encodedToken = encodedToken; } + /// + /// Called for each claim when token payload is being read. + /// + /// + /// An example implementation: + /// + /// 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); + /// } + /// + /// + internal ReadTokenPayloadValueDelegate ReadTokenPayloadValueDelegate { get; set; } = ReadTokenPayloadValue; + internal string ActualIssuer { get; set; } internal ClaimsIdentity ActorClaimsIdentity { get; set; } diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 876d267345..ac71a4c109 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; namespace Microsoft.IdentityModel.Tokens @@ -210,4 +211,13 @@ internal delegate ValidationResult SignatureValidationDelegate( BaseConfiguration? configuration, CallContext callContext); #nullable restore + + /// + /// Definition for ReadTokenPayloadValueDelegate. + /// Called for each claim when token payload is being read. + /// + /// Reader for the underlying token bytes. + /// The name of the claim being read. + /// + internal delegate object ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader, string claimName); } diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 6e9eef5fa4..b7be096460 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -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 diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 5ba076d00b..30b052387a 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -449,6 +449,11 @@ public string NameClaimType /// public IDictionary PropertyBag { get; set; } + /// + /// Gets or sets a delegate that will be called when reading token payload claims. + /// + internal ReadTokenPayloadValueDelegate ReadTokenPayloadValue { get; set; } + /// /// Gets or sets a boolean to control if configuration required to be refreshed before token validation. /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs deleted file mode 100644 index 813b82e4b3..0000000000 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.IdentityModel.Tokens.Json; - -namespace Microsoft.IdentityModel.JsonWebTokens.Tests -{ - public class CustomJsonWebToken : JsonWebToken - { - private const string CustomClaimName = "CustomClaim"; - - public CustomJsonWebToken(string jwtEncodedString) : base(jwtEncodedString) { } - - public CustomJsonWebToken(ReadOnlyMemory encodedTokenMemory) : base(encodedTokenMemory) { } - - public CustomJsonWebToken(string header, string payload) : base(header, payload) { } - - private protected override void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) - { - if (reader.ValueTextEquals(CustomClaimName)) - { - _customClaim = JsonSerializerPrimitives.ReadString(ref reader, CustomClaimName, ClassName, true); - claims[CustomClaimName] = _customClaim; - } - else - { - base.ReadPayloadValue(ref reader, claims); - } - } - - private string _customClaim; - - public string CustomClaim - { - get - { - _customClaim ??= Payload.GetStringValue(CustomClaimName); - return _customClaim; - } - } - } -} diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index e0c2133600..1817f1e675 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -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; @@ -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 { - { "CustomClaim", expectedCustomClaim }, + { "CustomPayload", "custom_payload" }, + }, + AdditionalHeaderClaims = new Dictionary + { + { "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("CustomHeader", out var actualHeaderClaim)); + Assert.True(jwt.TryGetPayloadValue("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; } }