Skip to content

Commit a22ab8e

Browse files
author
Franco Fung
committed
Merged PR 10603: Re-enable Jwt sub claim as either Number or String
This PR re-enables incoming Jwt's to set the 'sub' claim as a Number type in their payloads. Added a new method "ReadStringOrNumberAsString" in the JsonSerializerPrimitives. It will process the 'sub' claim that comes in either as String or Number and will always return it back as a string. Replaced 'sub' claim logic to leverage ReadNumberAsString method in JwtPayload.cs Replaced 'sub' claim logic to leverage ReadNumberAsString method in JsonWebToken.cs Fixes: #2325 ---- #### AI-Generated Description This pull request primarily focuses on modifying the way the `sub` claim is read from JSON payloads in the **JsonWebToken.PayloadClaimSet.cs** and **JwtPayload.cs** files. - In **JsonWebToken.PayloadClaimSet.cs**, the method `JsonSerializerPrimitives.ReadString` has been replaced with `JsonSerializerPrimitives.ReadStringOrNumberAsString` for reading the `sub` claim. This allows the `sub` claim to be read as a string or number, but always returned as a string. - In **JwtPayload.cs**, the logic for reading the `sub` claim has been simplified. The previous code handled multiple token types and threw an exception for unsupported types. The new code directly uses `JsonSerializerPrimitives.ReadStringOrNumberAsString`, simplifying the process. - The **JwtSecurityTokenHandlerTests.cs** file has been updated to remove a duplicate `sub` claim from the test data. - New tests have been added in **JsonWebTokenTests.cs** to validate the reading of the `sub` claim as a string or number. - A new exception type `JsonException` has been added in **ExpectedException.cs** to handle JSON related exceptions. - A new method `ReadStringOrNumberAsString` has been added in **JsonSerializerPrimitives.cs** to read a JSON token as a string or number and always return it as a string. This method is used to read the `sub` claim in the updated files. Related work items: #2753966
1 parent 1966c05 commit a22ab8e

File tree

6 files changed

+167
-37
lines changed

6 files changed

+167
-37
lines changed

src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ internal JsonClaimSet CreatePayloadClaimSet(byte[] bytes, int length)
8989
}
9090
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Sub))
9191
{
92-
_sub = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true);
92+
_sub = JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true);
9393
claims[JwtRegisteredClaimNames.Sub] = _sub;
9494
}
9595
else

src/Microsoft.IdentityModel.Tokens/Json/JsonSerializerPrimitives.cs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,33 @@ internal static string ReadString(ref Utf8JsonReader reader, string propertyName
648648
return reader.GetString();
649649
}
650650

651+
/// <summary>
652+
/// This method allows a JsonTokenType to be string or number but, it will always return it as a string.
653+
/// </summary>
654+
/// <param name="reader">The<see cref="Utf8JsonReader"/></param>
655+
/// <param name="propertyName">The property name that is being read.</param>
656+
/// <param name="className">The type that is being deserialized.</param>
657+
/// <param name="read">If true reader.Read() will be called.</param>
658+
/// <returns>Value from reader as string.</returns>
659+
internal static string ReadStringOrNumberAsString(ref Utf8JsonReader reader, string propertyName, string className, bool read = false)
660+
{
661+
if (read)
662+
reader.Read();
663+
664+
// returning null keeps the same logic as JsonSerialization.ReadObject
665+
if (reader.TokenType == JsonTokenType.Null)
666+
return null;
667+
668+
if (reader.TokenType == JsonTokenType.Number)
669+
return ReadNumber(ref reader).ToString();
670+
671+
if (reader.TokenType != JsonTokenType.String)
672+
throw LogHelper.LogExceptionMessage(
673+
CreateJsonReaderException(ref reader, "JsonTokenType.String or JsonTokenType.Number", className, propertyName));
674+
675+
return reader.GetString();
676+
}
677+
651678
internal static object ReadStringAsObject(ref Utf8JsonReader reader, string propertyName, string className, bool read = false)
652679
{
653680
if (read)
@@ -771,13 +798,13 @@ internal static void ReadStringsSkipNulls(
771798

772799
/// <summary>
773800
/// This method is called when deserializing a property value as an object.
774-
/// Normally we put the object into a Dictionary[string, object].
801+
/// Normally, we put the object into a Dictionary[string, object].
775802
/// </summary>
776-
/// <param name="reader">the <see cref="Utf8JsonReader"/></param>
777-
/// <param name="propertyName">the property name that is being read</param>
778-
/// <param name="className">the type that is being deserialized</param>
779-
/// <param name="read">if true reader.Read() will be called.</param>
780-
/// <returns></returns>
803+
/// <param name="reader">The <see cref="Utf8JsonReader"/></param>
804+
/// <param name="propertyName">The property name that is being read.</param>
805+
/// <param name="className">The type that is being deserialized.</param>
806+
/// <param name="read">If true reader.Read() will be called.</param>
807+
/// <returns>Value from reader as an object.</returns>
781808
internal static object ReadPropertyValueAsObject(ref Utf8JsonReader reader, string propertyName, string className, bool read = false)
782809
{
783810
if (read)

src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -114,31 +114,8 @@ internal static JwtPayload CreatePayload(byte[] bytes, int length)
114114
}
115115
else if (reader.ValueTextEquals(JwtPayloadUtf8Bytes.Sub))
116116
{
117-
reader.Read();
118-
if (reader.TokenType == JsonTokenType.String)
119-
{
120-
payload._sub = JsonSerializerPrimitives.ReadString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, false);
121-
payload[JwtRegisteredClaimNames.Sub] = payload._sub;
122-
}
123-
else if (reader.TokenType == JsonTokenType.StartArray)
124-
{
125-
payload._audiences = new List<string>();
126-
JsonSerializerPrimitives.ReadStrings(ref reader, payload._audiences, JwtRegisteredClaimNames.Sub, ClassName, false);
127-
payload[JwtRegisteredClaimNames.Sub] = payload._audiences;
128-
}
129-
else
130-
{
131-
throw LogHelper.LogExceptionMessage(
132-
new JsonException(
133-
LogHelper.FormatInvariant(
134-
Microsoft.IdentityModel.Tokens.LogMessages.IDX11023,
135-
LogHelper.MarkAsNonPII("JsonTokenType.String or JsonTokenType.StartArray"),
136-
LogHelper.MarkAsNonPII(reader.TokenType),
137-
LogHelper.MarkAsNonPII(ClassName),
138-
LogHelper.MarkAsNonPII(reader.TokenStartIndex),
139-
LogHelper.MarkAsNonPII(reader.CurrentDepth),
140-
LogHelper.MarkAsNonPII(reader.BytesConsumed))));
141-
}
117+
payload._sub = JsonSerializerPrimitives.ReadStringOrNumberAsString(ref reader, JwtRegisteredClaimNames.Sub, ClassName, true);
118+
payload[JwtRegisteredClaimNames.Sub] = payload._sub;
142119
}
143120
else
144121
{

test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenTests.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Reflection;
1313
using System.Security.Claims;
1414
using System.Text;
15+
using System.Text.Json;
1516
using Microsoft.IdentityModel.TestUtils;
1617
using Microsoft.IdentityModel.Tokens;
1718
using Microsoft.IdentityModel.Tokens.Json.Tests;
@@ -603,6 +604,131 @@ public void TryGetPayloadValue(GetPayloadValueTheoryData theoryData)
603604
TestUtilities.AssertFailIfErrors(context);
604605
}
605606

607+
608+
[Theory, MemberData(nameof(GetPayloadSubClaimValueTheoryData), DisableDiscoveryEnumeration = true)]
609+
public void GetPayloadSubClaimValue(GetPayloadValueTheoryData theoryData)
610+
{
611+
CompareContext context = TestUtilities.WriteHeader($"{this}.GetPayloadSubClaimValue", theoryData);
612+
try
613+
{
614+
JsonWebToken jsonWebToken = new JsonWebToken(theoryData.Json);
615+
string payload = Base64UrlEncoder.Decode(jsonWebToken.EncodedPayload);
616+
MethodInfo method = typeof(JsonWebToken).GetMethod("GetPayloadValue");
617+
MethodInfo generic = method.MakeGenericMethod(theoryData.PropertyType);
618+
object[] parameters = new object[] { theoryData.PropertyName };
619+
var retVal = generic.Invoke(jsonWebToken, parameters);
620+
621+
theoryData.ExpectedException.ProcessNoException(context);
622+
IdentityComparer.AreEqual(retVal, theoryData.PropertyValue, context);
623+
}
624+
catch (Exception ex)
625+
{
626+
theoryData.ExpectedException.ProcessException(ex.InnerException, context);
627+
}
628+
629+
TestUtilities.AssertFailIfErrors(context);
630+
}
631+
632+
public static TheoryData<GetPayloadValueTheoryData> GetPayloadSubClaimValueTheoryData
633+
{
634+
get
635+
{
636+
var theoryData = new TheoryData<GetPayloadValueTheoryData>();
637+
string[] stringArray = new string[] { "string1", "string2" };
638+
object propertyValue = new Dictionary<string, string[]> { { "stringArray", stringArray } };
639+
640+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsString")
641+
{
642+
PropertyName = "sub",
643+
PropertyType = typeof(string),
644+
PropertyValue = null,
645+
Json = JsonUtilities.CreateUnsignedToken("sub", null)
646+
});
647+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsBoolTrue")
648+
{
649+
PropertyName = "sub",
650+
PropertyType = typeof(bool),
651+
PropertyValue = true,
652+
Json = JsonUtilities.CreateUnsignedToken("sub", true),
653+
ExpectedException = ExpectedException.JsonException("IDX11020:")
654+
});
655+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsBoolFalse")
656+
{
657+
PropertyName = "sub",
658+
PropertyType = typeof(bool),
659+
PropertyValue = false,
660+
Json = JsonUtilities.CreateUnsignedToken("sub", false),
661+
ExpectedException = ExpectedException.JsonException("IDX11020:")
662+
});
663+
664+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsArray")
665+
{
666+
PropertyName = "sub",
667+
PropertyType = typeof(string[]),
668+
PropertyValue = stringArray,
669+
Json = JsonUtilities.CreateUnsignedToken("sub", stringArray),
670+
ExpectedException = ExpectedException.JsonException("IDX11020:")
671+
});
672+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsObject")
673+
{
674+
PropertyName = "sub",
675+
PropertyType = typeof(object),
676+
PropertyValue = propertyValue,
677+
Json = JsonUtilities.CreateUnsignedToken("sub", propertyValue),
678+
ExpectedException = ExpectedException.JsonException("IDX11020:")
679+
});
680+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsDouble")
681+
{
682+
PropertyName = "sub",
683+
PropertyType = typeof(double),
684+
PropertyValue = 622.101,
685+
Json = JsonUtilities.CreateUnsignedToken("sub", 622.101)
686+
});
687+
688+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsDecimal")
689+
{
690+
PropertyName = "sub",
691+
PropertyType = typeof(decimal),
692+
PropertyValue = 422.101,
693+
Json = JsonUtilities.CreateUnsignedToken("sub", 422.101)
694+
});
695+
696+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsFloat")
697+
{
698+
PropertyName = "sub",
699+
PropertyType = typeof(float),
700+
PropertyValue = 42.1,
701+
Json = JsonUtilities.CreateUnsignedToken("sub", 42.1)
702+
});
703+
704+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsInteger")
705+
{
706+
PropertyName = "sub",
707+
PropertyType = typeof(int),
708+
PropertyValue = 42,
709+
Json = JsonUtilities.CreateUnsignedToken("sub", 42)
710+
});
711+
712+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsUInt")
713+
{
714+
PropertyName = "sub",
715+
PropertyType = typeof(uint),
716+
PropertyValue = 540,
717+
Json = JsonUtilities.CreateUnsignedToken("sub", 540)
718+
});
719+
720+
theoryData.Add(new GetPayloadValueTheoryData("SubjectAsUlong")
721+
{
722+
PropertyName = "sub",
723+
PropertyType = typeof(ulong),
724+
PropertyValue = 642,
725+
Json = JsonUtilities.CreateUnsignedToken("sub", 642)
726+
});
727+
728+
return theoryData;
729+
}
730+
731+
}
606732
// This test ensures that accessing claims from the payload works as expected.
607733
[Theory, MemberData(nameof(GetPayloadValueTheoryData), DisableDiscoveryEnumeration = true)]
608734
public void GetPayloadValue(GetPayloadValueTheoryData theoryData)

test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Reflection;
88
using System.Security.Cryptography;
9+
using System.Text.Json;
910
using System.Xml;
1011
using Microsoft.IdentityModel.Tokens;
1112

@@ -304,6 +305,10 @@ public static ExpectedException KeyWrapException(string substringExpected = null
304305
return new ExpectedException(typeof(SecurityTokenKeyWrapException), substringExpected, innerTypeExpected);
305306
}
306307

308+
public static ExpectedException JsonException(string substringExpected = null, Type innerTypeExpected = null)
309+
{
310+
return new ExpectedException(typeof(JsonException), substringExpected, innerTypeExpected);
311+
}
307312
public bool IgnoreExceptionType { get; set; } = false;
308313

309314
public bool IgnoreInnerException { get; set; }

test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,6 @@ public void InboundOutboundClaimTypeMapping()
874874
new Claim( ClaimTypes.Spn, "spn", ClaimValueTypes.String, Default.Issuer, Default.Issuer ),
875875
new Claim( JwtRegisteredClaimNames.Sub, "Subject1", ClaimValueTypes.String, Default.Issuer, Default.Issuer ),
876876
new Claim( JwtRegisteredClaimNames.Prn, "Principal1", ClaimValueTypes.String, Default.Issuer, Default.Issuer ),
877-
new Claim( JwtRegisteredClaimNames.Sub, "Subject2", ClaimValueTypes.String, Default.Issuer, Default.Issuer ),
878877
};
879878

880879

@@ -912,10 +911,6 @@ public void InboundOutboundClaimTypeMapping()
912911
claim.Properties.Add(new KeyValuePair<string, string>(JwtSecurityTokenHandler.ShortClaimTypeProperty, JwtRegisteredClaimNames.Prn));
913912
expectedClaims.Add(claim);
914913

915-
claim = new Claim("Mapped_" + JwtRegisteredClaimNames.Sub, "Subject2", ClaimValueTypes.String, Default.Issuer, Default.Issuer);
916-
claim.Properties.Add(new KeyValuePair<string, string>(JwtSecurityTokenHandler.ShortClaimTypeProperty, JwtRegisteredClaimNames.Sub));
917-
expectedClaims.Add(claim);
918-
919914
RunClaimMappingVariation(jwt, handler, validationParameters, expectedClaims: expectedClaims, identityName: null);
920915
}
921916

0 commit comments

Comments
 (0)