From aecf7906c224cdc60f1e78f1f1a5280e12604d47 Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:19:06 -0800 Subject: [PATCH 1/2] Replace JsonWebToken ReadPayloadValue with a delegate --- .../InternalAPI.Unshipped.txt | 6 + .../Json/JsonClaimSet.cs | 2 +- .../Json/JsonWebToken.PayloadClaimSet.cs | 37 +++--- .../JsonWebToken.cs | 106 ++++++++++++++++++ .../JsonWebTokenHandler.cs | 2 +- .../Delegates.cs | 10 ++ .../TokenValidationParameters.cs | 5 + .../CustomJsonWebToken.cs | 59 ---------- .../JsonWebTokenTests.cs | 102 +++++++++++++++-- 9 files changed, 236 insertions(+), 93 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 ff770358f8..7f8e906007 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt @@ -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> 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.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 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 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 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..1f3904aa0c 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,7 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan 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)) @@ -52,11 +51,17 @@ 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. + /// A collection to hold claims that have been read. + /// A claim that was read. + internal static void ReadTokenPayloadValue(ref Utf8JsonReader reader, IDictionary claims) { if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) { - _audiences = []; + List _audiences = []; reader.Read(); if (reader.TokenType == JsonTokenType.StartArray) { @@ -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 { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 25a220641a..b21fe823ea 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -83,6 +83,33 @@ public JsonWebToken(string jwtEncodedString) _encodedToken = jwtEncodedString; } + /// + /// Initializes a new instance of from a string in JWS or JWE Compact serialized format. + /// + /// A JSON Web Token that has been serialized in JWS or JWE Compact serialized format. + /// A custom delegate to be called when each payload claim is being read. If null, default implementation is called. + /// Thrown if is null or empty. + /// Thrown if is not in JWS or JWE Compact Serialization format. + /// + /// See: (JWT). + /// See: (JWS). + /// See: (JWE). + /// + /// The contents of the returned have not been validated, the JSON Web Token is simply decoded. Validation can be accomplished using the validation methods in + /// + /// + 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; + } + /// /// Initializes a new instance of from a ReadOnlyMemory{char} in JWS or JWE Compact serialized format. /// @@ -107,6 +134,33 @@ public JsonWebToken(ReadOnlyMemory encodedTokenMemory) _encodedTokenMemory = encodedTokenMemory; } + /// + /// 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. + /// Thrown if is empty. + /// Thrown if does not represent a valid JWS or JWE Compact Serialization format. + /// + /// See: (JWT). + /// See: (JWS). + /// See: (JWE). + /// + /// The contents of the returned have not been validated; the JSON Web Token is simply decoded. Validation can be performed using the methods in . + /// + /// + 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 the class where the header contains the crypto algorithms applied to the encoded header and payload. /// @@ -138,6 +192,58 @@ public JsonWebToken(string header, string payload) _encodedToken = encodedToken; } + /// + /// Initializes a new instance of the class where the header contains the crypto algorithms applied to the encoded header and payload. + /// + /// A string containing JSON which represents the cryptographic operations applied to the JWT and optionally any additional properties of the JWT. + /// 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. + /// A custom delegate to be called when each payload claim is being read. If null, default implementation is called. + /// + /// See: (JWT). + /// See: (JWS). + /// See: (JWE). + /// + /// The contents of the returned have not been validated, the JSON Web Token is simply decoded. Validation can be accomplished using the validation methods in + /// + /// + /// Thrown if is null or empty. + /// Thrown if is null. + 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; + } + + /// + /// 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.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 20b294f7a1..29e13029b9 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -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) { diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 876d267345..08d174dd3b 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. + /// A collection to hold claims that have been read. + /// + internal delegate void ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader, IDictionary claims); } 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 f0e03afcf1..0000000000 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/CustomJsonWebToken.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Text.Json; - -namespace Microsoft.IdentityModel.JsonWebTokens.Tests -{ - public class CustomJsonWebToken : JsonWebToken - { - // Represents claims known to this custom implementation and not to the IdentityModel. - public const string CustomClaimName = "CustomClaim"; - - private CustomClaim _customClaim; - - public CustomClaim CustomClaim - { - get - { - _customClaim ??= Payload.GetValue(CustomClaimName); - return _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) - { - // Handle custom claims. - if (reader.ValueTextEquals(CustomClaimName)) - { - // Deserialize the custom object claim in an appropriate way. - reader.Read(); // Move to the value. - _customClaim = JsonSerializer.Deserialize(reader.GetString()); - claims[CustomClaimName] = _customClaim; - reader.Read(); - } - else - { - // Call base implementation to handle other claims known to IdentityModel. - base.ReadPayloadValue(ref reader, claims); - } - } - } - - public class CustomClaim - { - public CustomClaim() - { - } - - public string CustomClaimValue { get; set; } - } -} diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 185cb1f860..07155ea4f3 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -12,7 +12,9 @@ using System.Reflection; using System.Security.Claims; using System.Text; +using System.Text.Json; using System.Threading; +using System.Threading.Tasks; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens.Json.Tests; @@ -1741,25 +1743,103 @@ public void StringAndMemoryConstructors_CreateEquivalentTokens(JwtTheoryData the } [Fact] - public void DerivedJsonWebToken_IsCreatedCorrectly() + public void ReadTokenDelegates_UsingJsonWebToken_CalledCorrectly() { - var expectedCustomClaim = new CustomClaim() { CustomClaimValue = "customclaim" }; - var tokenStr = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor + var customPayloadClaimName = "CustomPayload"; + + var expectedCustomPayloadClaim = new CustomPayloadClaim("custom_payload"); + + var tokenSpan = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor { Issuer = Default.Issuer, Claims = new Dictionary { - { CustomJsonWebToken.CustomClaimName, System.Text.Json.JsonSerializer.Serialize(expectedCustomClaim) }, + { customPayloadClaimName, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim) }, } - }); + }).AsMemory(); + + CustomPayloadClaim payloadClaimFromDelegate = null; + void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) + { + // Handle custom claims. + if (reader.ValueTextEquals(customPayloadClaimName)) + { + reader.Read(); // Move to the value. + payloadClaimFromDelegate = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); + claims[customPayloadClaimName] = payloadClaimFromDelegate; + reader.Read(); + } + else + { + // Call base implementation to handle other claims known to IdentityModel. + JsonWebToken.ReadTokenPayloadValue(ref reader, claims); + } + } - var derivedToken = new CustomJsonWebToken(tokenStr); - derivedToken.TryGetPayloadValue( - CustomJsonWebToken.CustomClaimName, out CustomClaim customClaim); + var jwt = new JsonWebToken(tokenSpan, ReadPayloadValue); - Assert.Equal(expectedCustomClaim.CustomClaimValue, derivedToken.CustomClaim.CustomClaimValue); - Assert.Equal(expectedCustomClaim.CustomClaimValue, customClaim.CustomClaimValue); - Assert.Equal(Default.Issuer, derivedToken.Issuer); + Assert.True(jwt.TryGetPayloadValue(customPayloadClaimName, out var actualPayloadClaim)); + Assert.Equal(expectedCustomPayloadClaim.Value, actualPayloadClaim.Value); + Assert.NotNull(payloadClaimFromDelegate); + Assert.Equal(expectedCustomPayloadClaim.Value, payloadClaimFromDelegate.Value); + } + + [Fact] + public async Task ReadTokenDelegates_UsingJsonWebTokenHandler_CalledCorrectly() + { + var customPayloadClaimName = "CustomPayload"; + + var expectedCustomPayloadClaim = new CustomPayloadClaim("custom_payload"); + + var tokenSpan = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor + { + Issuer = Default.Issuer, + Claims = new Dictionary + { + { customPayloadClaimName, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim) }, + }, + SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, + }).AsMemory(); + + CustomPayloadClaim payloadClaimFromDelegate = null; + void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) + { + // Handle custom claims. + if (reader.ValueTextEquals(customPayloadClaimName)) + { + reader.Read(); // Move to the value. + payloadClaimFromDelegate = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); + claims[customPayloadClaimName] = payloadClaimFromDelegate; + reader.Read(); + } + else + { + // Call base implementation to handle other claims known to IdentityModel. + JsonWebToken.ReadTokenPayloadValue(ref reader, claims); + } + } + + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = false, + IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, + ReadTokenPayloadValue = ReadPayloadValue, + }; + + var validationResult = await new JsonWebTokenHandler().ValidateTokenAsync(tokenSpan.ToString(), tokenValidationParameters); + + Assert.True(((JsonWebToken)validationResult.SecurityToken).TryGetPayloadValue(customPayloadClaimName, out var actualPayloadClaim)); + Assert.Equal(expectedCustomPayloadClaim.Value, actualPayloadClaim.Value); + Assert.NotNull(payloadClaimFromDelegate); + Assert.Equal(expectedCustomPayloadClaim.Value, payloadClaimFromDelegate.Value); + } + + private class CustomPayloadClaim(string value) + { + public string Value { get; set; } = value; } [Fact] From c8207d29c6e5bcf0c374a950d89fae4ff901209a Mon Sep 17 00:00:00 2001 From: Peter <34331512+pmaytak@users.noreply.github.com> Date: Tue, 11 Feb 2025 00:33:40 -0800 Subject: [PATCH 2/2] Refactor the single delegate approach into a collection of delegates. --- .../InternalAPI.Unshipped.txt | 11 +- .../Json/JsonWebToken.PayloadClaimSet.cs | 131 +++++++++--------- .../JsonWebToken.cs | 31 ++--- .../JsonWebTokenHandler.cs | 2 +- .../Delegates.cs | 10 +- .../TokenValidationParameters.cs | 8 +- .../JsonWebTokenTests.cs | 108 ++++++++------- .../TokenValidationParametersTests.cs | 8 +- 8 files changed, 160 insertions(+), 149 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt index 7f8e906007..dc8e42bd0d 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt @@ -1,12 +1,11 @@ +Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string header, string payload, System.Collections.Generic.IDictionary readTokenPayloadValueDelegates) -> void +Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string jwtEncodedString, System.Collections.Generic.IDictionary readTokenPayloadValueDelegates) -> void +Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(System.ReadOnlyMemory encodedTokenMemory, System.Collections.Generic.IDictionary readTokenPayloadValueDelegates) -> void +Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.ReadTokenPayloadValueDelegates.get -> System.Collections.Generic.IDictionary +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> 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.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 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 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 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/JsonWebToken.PayloadClaimSet.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs index 1f3904aa0c..41c8af56ca 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs @@ -39,7 +39,71 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) { if (reader.TokenType == JsonTokenType.PropertyName) { - ReadTokenPayloadValueDelegate(ref reader, claims); + if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) + { + 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; + } + } + } + else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) + { + 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)) @@ -50,70 +114,5 @@ internal JsonClaimSet CreatePayloadClaimSet(ReadOnlySpan byteSpan) return new JsonClaimSet(claims); } - - /// - /// Reads and saves the value of the payload claim from the reader. - /// - /// The reader over the JWT. - /// A collection to hold claims that have been read. - /// A claim that was read. - internal static void ReadTokenPayloadValue(ref Utf8JsonReader reader, IDictionary claims) - { - if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Aud)) - { - 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; - } - } - } - else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Azp)) - { - 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(); - claims[propertyName] = JsonSerializerPrimitives.ReadPropertyValueAsObject(ref reader, propertyName, JsonClaimSet.ClassName, true); - } - } } } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index b21fe823ea..619c0acc65 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -87,7 +87,7 @@ public JsonWebToken(string jwtEncodedString) /// Initializes a new instance of from a string in JWS or JWE Compact serialized format. /// /// A JSON Web Token that has been serialized in JWS or JWE Compact serialized format. - /// A custom delegate to be called when each payload claim is being read. If null, default implementation is called. + /// Custom delegates to be called when reading an associated claim names. /// Thrown if is null or empty. /// Thrown if is not in JWS or JWE Compact Serialization format. /// @@ -98,12 +98,12 @@ public JsonWebToken(string jwtEncodedString) /// The contents of the returned have not been validated, the JSON Web Token is simply decoded. Validation can be accomplished using the validation methods in /// /// - internal JsonWebToken(string jwtEncodedString, ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) + internal JsonWebToken(string jwtEncodedString, IDictionary readTokenPayloadValueDelegates) { if (string.IsNullOrEmpty(jwtEncodedString)) throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(jwtEncodedString))); - ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue; + ReadTokenPayloadValueDelegates = readTokenPayloadValueDelegates ?? new Dictionary(); ReadToken(jwtEncodedString.AsMemory()); @@ -138,7 +138,7 @@ public JsonWebToken(ReadOnlyMemory encodedTokenMemory) /// 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. + /// Custom delegates to be called when reading an associated claim names. /// Thrown if is empty. /// Thrown if does not represent a valid JWS or JWE Compact Serialization format. /// @@ -149,12 +149,12 @@ public JsonWebToken(ReadOnlyMemory encodedTokenMemory) /// The contents of the returned have not been validated; the JSON Web Token is simply decoded. Validation can be performed using the methods in . /// /// - internal JsonWebToken(ReadOnlyMemory encodedTokenMemory, ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) + internal JsonWebToken(ReadOnlyMemory encodedTokenMemory, IDictionary readTokenPayloadValueDelegates) { if (encodedTokenMemory.IsEmpty) throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(encodedTokenMemory))); - ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue; + ReadTokenPayloadValueDelegates = readTokenPayloadValueDelegates ?? new Dictionary(); ReadToken(encodedTokenMemory); @@ -197,7 +197,7 @@ public JsonWebToken(string header, string payload) /// /// A string containing JSON which represents the cryptographic operations applied to the JWT and optionally any additional properties of the JWT. /// 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. - /// A custom delegate to be called when each payload claim is being read. If null, default implementation is called. + /// Custom delegates to be called when reading an associated claim names. /// /// See: (JWT). /// See: (JWS). @@ -208,7 +208,7 @@ public JsonWebToken(string header, string payload) /// /// Thrown if is null or empty. /// Thrown if is null. - internal JsonWebToken(string header, string payload, ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) + internal JsonWebToken(string header, string payload, IDictionary readTokenPayloadValueDelegates) { if (string.IsNullOrEmpty(header)) throw LogHelper.LogArgumentNullException(nameof(header)); @@ -219,7 +219,7 @@ internal JsonWebToken(string header, string payload, ReadTokenPayloadValueDelega var encodedPayload = Base64UrlEncoder.Encode(payload); var encodedToken = encodedHeader + "." + encodedPayload + "."; - ReadTokenPayloadValueDelegate = readTokenPayloadValueDelegate ?? ReadTokenPayloadValue; + ReadTokenPayloadValueDelegates = readTokenPayloadValueDelegates ?? new Dictionary(); ReadToken(encodedToken.AsMemory()); @@ -227,22 +227,19 @@ internal JsonWebToken(string header, string payload, ReadTokenPayloadValueDelega } /// - /// Called for each claim when token payload is being read. + /// Gets or sets the 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. /// /// /// An example implementation: /// - /// object ReadPayloadValueDelegate(ref Utf8JsonReader reader, string claimName) => + /// object ReadPayloadValueDelegate(ref Utf8JsonReader reader) => /// { - /// if (reader.ValueTextEquals("CustomProp")) - /// { - /// return JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.CustomProp, ClassName, true); - /// } - /// return JsonWebToken.ReadTokenPayloadValue(ref reader, claimName); + /// return JsonSerializer.Deserialize<CustomPayloadClaim>(reader.GetString()); /// } /// /// - internal ReadTokenPayloadValueDelegate ReadTokenPayloadValueDelegate { get; set; } = ReadTokenPayloadValue; + internal IDictionary ReadTokenPayloadValueDelegates { get; set; } = new Dictionary(); internal string ActualIssuer { get; set; } diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index 29e13029b9..2acfc8f697 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -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, validationParameters.ReadTokenPayloadValue); + jsonWebToken = new JsonWebToken(token, validationParameters.ReadTokenPayloadValueDelegates); } catch (Exception ex) { diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 08d174dd3b..0d4bf09ccd 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -213,11 +213,11 @@ internal delegate ValidationResult SignatureValidationDelegate( #nullable restore /// - /// Definition for ReadTokenPayloadValueDelegate. - /// Called for each claim when token payload is being read. + /// 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. /// /// Reader for the underlying token bytes. - /// A collection to hold claims that have been read. - /// - internal delegate void ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader, IDictionary claims); + /// Claim value associated with the claim name. + internal delegate object ReadTokenPayloadValueDelegate(ref Utf8JsonReader reader); } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index 30b052387a..8db5af1bba 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -73,6 +73,7 @@ protected TokenValidationParameters(TokenValidationParameters other) NameClaimType = other.NameClaimType; NameClaimTypeRetriever = other.NameClaimTypeRetriever; PropertyBag = other.PropertyBag; + ReadTokenPayloadValueDelegates = other.ReadTokenPayloadValueDelegates; RefreshBeforeValidation = other.RefreshBeforeValidation; RequireAudience = other.RequireAudience; RequireExpirationTime = other.RequireExpirationTime; @@ -450,9 +451,10 @@ public string NameClaimType public IDictionary PropertyBag { get; set; } /// - /// Gets or sets a delegate that will be called when reading token payload claims. + /// Gets or sets the 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. /// - internal ReadTokenPayloadValueDelegate ReadTokenPayloadValue { get; set; } + internal IDictionary ReadTokenPayloadValueDelegates { get; set; } /// /// Gets or sets a boolean to control if configuration required to be refreshed before token validation. @@ -560,7 +562,7 @@ public string RoleClaimType public SecurityKey TokenDecryptionKey { get; set; } /// - /// Gets or sets a delegate that will be called to retreive a used for decryption. + /// Gets or sets a delegate that will be called to retrieve a used for decryption. /// /// /// This will be used to decrypt the token. This can be helpful when the does not contain a key identifier. diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs index 07155ea4f3..f3fdb42c64 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs @@ -1745,78 +1745,85 @@ public void StringAndMemoryConstructors_CreateEquivalentTokens(JwtTheoryData the [Fact] public void ReadTokenDelegates_UsingJsonWebToken_CalledCorrectly() { - var customPayloadClaimName = "CustomPayload"; - - var expectedCustomPayloadClaim = new CustomPayloadClaim("custom_payload"); + var customPayloadClaimName1 = "CustomPayload1"; + var customPayloadClaimName2 = "CustomPayload2"; + var expectedCustomPayloadClaim1 = new CustomPayloadClaim("custom_payload1"); + var expectedCustomPayloadClaim2 = new CustomPayloadClaim("custom_payload2"); var tokenSpan = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor { Issuer = Default.Issuer, Claims = new Dictionary { - { customPayloadClaimName, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim) }, + { customPayloadClaimName1, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim1) }, + { customPayloadClaimName2, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim2) }, } }).AsMemory(); - CustomPayloadClaim payloadClaimFromDelegate = null; - void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) + CustomPayloadClaim payloadClaimFromDelegate1 = null; + CustomPayloadClaim payloadClaimFromDelegate2 = null; + object ReadCustomPayloadClaimValue1(ref Utf8JsonReader reader) { // Handle custom claims. - if (reader.ValueTextEquals(customPayloadClaimName)) - { - reader.Read(); // Move to the value. - payloadClaimFromDelegate = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); - claims[customPayloadClaimName] = payloadClaimFromDelegate; - reader.Read(); - } - else - { - // Call base implementation to handle other claims known to IdentityModel. - JsonWebToken.ReadTokenPayloadValue(ref reader, claims); - } + payloadClaimFromDelegate1 = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); + return payloadClaimFromDelegate1; + } + object ReadCustomPayloadClaimValue2(ref Utf8JsonReader reader) + { + // Handle custom claims. + payloadClaimFromDelegate2 = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); + return payloadClaimFromDelegate2; } - var jwt = new JsonWebToken(tokenSpan, ReadPayloadValue); - - Assert.True(jwt.TryGetPayloadValue(customPayloadClaimName, out var actualPayloadClaim)); - Assert.Equal(expectedCustomPayloadClaim.Value, actualPayloadClaim.Value); - Assert.NotNull(payloadClaimFromDelegate); - Assert.Equal(expectedCustomPayloadClaim.Value, payloadClaimFromDelegate.Value); + var jwt = new JsonWebToken( + tokenSpan, + new Dictionary { + { customPayloadClaimName1, ReadCustomPayloadClaimValue1 }, + { customPayloadClaimName2, ReadCustomPayloadClaimValue2 } }); + + Assert.True(jwt.TryGetPayloadValue(customPayloadClaimName1, out var actualPayloadClaim1)); + Assert.Equal(expectedCustomPayloadClaim1.Value, actualPayloadClaim1.Value); + Assert.NotNull(payloadClaimFromDelegate1); + Assert.Equal(expectedCustomPayloadClaim1.Value, payloadClaimFromDelegate1.Value); + + Assert.True(jwt.TryGetPayloadValue(customPayloadClaimName2, out var actualPayloadClaim2)); + Assert.Equal(expectedCustomPayloadClaim2.Value, actualPayloadClaim2.Value); + Assert.NotNull(payloadClaimFromDelegate2); + Assert.Equal(expectedCustomPayloadClaim2.Value, payloadClaimFromDelegate2.Value); } [Fact] public async Task ReadTokenDelegates_UsingJsonWebTokenHandler_CalledCorrectly() { - var customPayloadClaimName = "CustomPayload"; - - var expectedCustomPayloadClaim = new CustomPayloadClaim("custom_payload"); + var customPayloadClaimName1 = "CustomPayload1"; + var customPayloadClaimName2 = "CustomPayload2"; + var expectedCustomPayloadClaim1 = new CustomPayloadClaim("custom_payload1"); + var expectedCustomPayloadClaim2 = new CustomPayloadClaim("custom_payload2"); var tokenSpan = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor { Issuer = Default.Issuer, Claims = new Dictionary { - { customPayloadClaimName, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim) }, + { customPayloadClaimName1, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim1) }, + { customPayloadClaimName2, System.Text.Json.JsonSerializer.Serialize(expectedCustomPayloadClaim2) }, }, SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, }).AsMemory(); - CustomPayloadClaim payloadClaimFromDelegate = null; - void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary claims) + CustomPayloadClaim payloadClaimFromDelegate1 = null; + CustomPayloadClaim payloadClaimFromDelegate2 = null; + object ReadCustomPayloadClaimValue1(ref Utf8JsonReader reader) { // Handle custom claims. - if (reader.ValueTextEquals(customPayloadClaimName)) - { - reader.Read(); // Move to the value. - payloadClaimFromDelegate = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); - claims[customPayloadClaimName] = payloadClaimFromDelegate; - reader.Read(); - } - else - { - // Call base implementation to handle other claims known to IdentityModel. - JsonWebToken.ReadTokenPayloadValue(ref reader, claims); - } + payloadClaimFromDelegate1 = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); + return payloadClaimFromDelegate1; + } + object ReadCustomPayloadClaimValue2(ref Utf8JsonReader reader) + { + // Handle custom claims. + payloadClaimFromDelegate2 = System.Text.Json.JsonSerializer.Deserialize(reader.GetString()); + return payloadClaimFromDelegate2; } var tokenValidationParameters = new TokenValidationParameters @@ -1826,15 +1833,22 @@ void ReadPayloadValue(ref Utf8JsonReader reader, IDictionary cla ValidateLifetime = false, ValidateIssuerSigningKey = false, IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, - ReadTokenPayloadValue = ReadPayloadValue, + ReadTokenPayloadValueDelegates = new Dictionary { + { customPayloadClaimName1, ReadCustomPayloadClaimValue1 }, + { customPayloadClaimName2, ReadCustomPayloadClaimValue2 }}, }; var validationResult = await new JsonWebTokenHandler().ValidateTokenAsync(tokenSpan.ToString(), tokenValidationParameters); - Assert.True(((JsonWebToken)validationResult.SecurityToken).TryGetPayloadValue(customPayloadClaimName, out var actualPayloadClaim)); - Assert.Equal(expectedCustomPayloadClaim.Value, actualPayloadClaim.Value); - Assert.NotNull(payloadClaimFromDelegate); - Assert.Equal(expectedCustomPayloadClaim.Value, payloadClaimFromDelegate.Value); + Assert.True(((JsonWebToken)validationResult.SecurityToken).TryGetPayloadValue(customPayloadClaimName1, out var actualPayloadClaim1)); + Assert.Equal(expectedCustomPayloadClaim1.Value, actualPayloadClaim1.Value); + Assert.NotNull(payloadClaimFromDelegate1); + Assert.Equal(expectedCustomPayloadClaim1.Value, payloadClaimFromDelegate1.Value); + + Assert.True(((JsonWebToken)validationResult.SecurityToken).TryGetPayloadValue(customPayloadClaimName2, out var actualPayloadClaim2)); + Assert.Equal(expectedCustomPayloadClaim2.Value, actualPayloadClaim2.Value); + Assert.NotNull(payloadClaimFromDelegate2); + Assert.Equal(expectedCustomPayloadClaim2.Value, payloadClaimFromDelegate2.Value); } private class CustomPayloadClaim(string value) diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 0728420d51..1af4df45e3 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,12 +15,12 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 60; + int ExpectedPropertyCount = 61; // GetSets() compares the total property count which includes internal properties, against a list of public properties, minus delegates. // This allows us to keep track of any properties we are including in the total that are not public nor delegates. // Remove if/once we make TimeProvider public. As the GetSets() test will fail. - List internalNonDelegateProperties = new() { "TimeProvider" }; + List internalNonDelegateProperties = new() { "TimeProvider", "ReadTokenPayloadValueDelegates" }; [Fact] public void Publics() @@ -135,7 +135,7 @@ public void Publics() var compareContext = new CompareContext(); IdentityComparer.AreEqual(validationParametersInline, validationParametersSets, compareContext); - // only exlude 'IsClone' when comparing Clone vs. Original. + // only exclude 'IsClone' when comparing Clone vs. Original. var instanceContext = new CompareContext(); instanceContext.PropertiesToIgnoreWhenComparing.Add(typeof(TokenValidationParameters), new List { "IsClone" }); TokenValidationParameters validationParametersInLineClone = validationParametersInline.Clone(); @@ -240,7 +240,7 @@ public void GetSets() Object = validationParameters, }; - // check that we have checked all properties, subract the number of delegates. + // check that we have checked all properties, subtract the number of delegates. if (context.PropertyNamesAndSetGetValue.Count != ExpectedPropertyCount - delegates.Count - internalNonDelegateProperties.Count) compareContext.AddDiff($"Number of properties being set is: {context.PropertyNamesAndSetGetValue.Count}, number of properties is: {properties.Length - delegates.Count} (#Properties - #Delegates), adjust tests");