diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f9f664c9..ef50060eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ - TBD +# 10.0.2 + +- Disallowed Encode(payload) with AddClaim(s) + # 10.0.1 - Fixed deserializing JWT header diff --git a/src/JWT.Extensions.AspNetCore/JWT.Extensions.AspNetCore.csproj b/src/JWT.Extensions.AspNetCore/JWT.Extensions.AspNetCore.csproj index 52a7567d2..be0218711 100644 --- a/src/JWT.Extensions.AspNetCore/JWT.Extensions.AspNetCore.csproj +++ b/src/JWT.Extensions.AspNetCore/JWT.Extensions.AspNetCore.csproj @@ -10,7 +10,7 @@ Alexander Batishchev jwt;json;asp.net;asp.net core;.net core;authorization MIT - 10.1.0 + 10.1.1 10.0.0.0 10.0.0.0 JWT.Extensions.AspNetCore diff --git a/src/JWT.Extensions.DependencyInjection/JWT.Extensions.DependencyInjection.csproj b/src/JWT.Extensions.DependencyInjection/JWT.Extensions.DependencyInjection.csproj index eb63076bc..aed93e459 100644 --- a/src/JWT.Extensions.DependencyInjection/JWT.Extensions.DependencyInjection.csproj +++ b/src/JWT.Extensions.DependencyInjection/JWT.Extensions.DependencyInjection.csproj @@ -11,7 +11,7 @@ Alexander Batishchev jwt;json;asp.net;asp.net core;.net core;authorization;dependenсy injection MIT - 2.2.2 + 2.2.3 2.0.0.0 2.0.0.0 JWT diff --git a/src/JWT/Builder/JwtBuilder.cs b/src/JWT/Builder/JwtBuilder.cs index 36f6c61f4..f14adb965 100644 --- a/src/JWT/Builder/JwtBuilder.cs +++ b/src/JWT/Builder/JwtBuilder.cs @@ -1,8 +1,14 @@ -using System; +using System; using System.Linq; using System.Reflection; using JWT.Algorithms; using JWT.Serializers; +using Newtonsoft.Json; + +#if MODERN_DOTNET +using System.Text.Json.Serialization; +#endif + using static JWT.Internal.EncodingHelper; namespace JWT.Builder @@ -263,27 +269,19 @@ public string Encode() return _encoder.Encode(_jwt.Header, _jwt.Payload, _secrets?[0]); } - public string Encode(T payload) => - Encode(typeof(T), payload); - - public string Encode(Type payloadType, object payload) + public string Encode(object payload) { - if (payloadType is null) - throw new ArgumentNullException(nameof(payloadType)); if (payload is null) throw new ArgumentNullException(nameof(payload)); - - EnsureCanEncode(); - var dic = payloadType.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .ToDictionary(prop => prop.Name, prop => prop.GetValue(payload, null)); + EnsureCanEncode(); - foreach (var pair in dic) + if (_jwt.Payload.Any()) { - _jwt.Payload.Add(pair.Key, pair.Value); + throw new NotSupportedException("Supplying both key-value pairs and implicit payload is not supported."); } - return Encode(); + return _encoder.Encode(_jwt.Header, payload, _secrets?[0]); } /// @@ -320,6 +318,14 @@ public T DecodeHeader(string token) return _decoder.DecodeHeader(token); } + public object Decode(string token, Type type) + { + EnsureCanDecode(); + + return _decoder.DecodeToObject(type, token, _secrets, _valParams.ValidateSignature); + } + + /// /// Decodes a token using the supplied dependencies. /// @@ -468,5 +474,42 @@ private bool CanDecodeHeader() return true; } + + private string GetPropName(MemberInfo prop) + { + var jsonSerializer = _jsonSerializerFactory.Create(); + + var customAttributes = prop.GetCustomAttributes(inherit: true); + foreach (var attribute in customAttributes) + { + switch (jsonSerializer) + { + case JsonNetSerializer: + { + if (attribute is JsonPropertyAttribute jsonNetProperty) + { + return jsonNetProperty.PropertyName; + } + break; + } +#if MODERN_DOTNET + case SystemTextSerializer: + { + if (attribute is JsonPropertyNameAttribute stjProperty) + { + return stjProperty.Name; + } + break; + } +#endif + default: + { + return prop.Name; + } + } + } + + return prop.Name; + } } } diff --git a/src/JWT/JWT.csproj b/src/JWT/JWT.csproj index 7513c1e4a..579726def 100644 --- a/src/JWT/JWT.csproj +++ b/src/JWT/JWT.csproj @@ -1,4 +1,4 @@ - + netstandard1.3;netstandard2.0;net6.0;net35;net40;net462; @@ -20,7 +20,7 @@ Alexander Batishchev, John Sheehan, Michael Lehenbauer jwt;json;authorization CC0-1.0 - 10.0.1 + 10.0.2 10.0.0.0 10.0.0.0 JWT diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs index dbafb02cf..cc88ba68a 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs @@ -433,5 +433,115 @@ public void Decode_ToDictionary_Without_Serializer_Should_Throw_Exception() action.Should() .Throw(); } + + [TestMethod] + public void Encode_Decode_ToJsonNetDecoratedType_Should_UseDecoratedName_Bug456() + { + var serializer = new JsonNetSerializer(); + var alg = new NoneAlgorithm(); + + var token = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer); + + var expected = new TestData.TestDataJsonNetDecorated + { + City = "Amsterdam", + }; + + var encoded = token.Encode(expected); + encoded.Should().NotBeNullOrEmpty(); + + token = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer); + + var actual = token.Decode(encoded); + actual.Should().BeEquivalentTo(expected); + } + + [TestMethod] + public void Encode_Decode_Should_Return_Token_Nested_Data_ShouldRespectAttributes() + { + var serializer = new JsonNetSerializer(); + var alg = new NoneAlgorithm(); + + var expected = new TestData.TestDataJsonNetDecorated + { + City = "Amsterdam", + }; + + var encoded = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer) + .AddClaim("Data", expected) + .Encode(); + + encoded.Should().NotBeNullOrEmpty(); + Console.WriteLine(encoded); + var jwtBuilder = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer); + + var payloadType = new TestData.PayloadWithNestedJsonNetData + { + Data = default + }; + + var actual = (TestData.PayloadWithNestedJsonNetData)jwtBuilder.Decode(encoded, payloadType.GetType()); + actual.Data.Should().BeEquivalentTo(expected); + } + +#if NETSTANDARD2_0 || NET6_0_OR_GREATER + [TestMethod] + public void Encode_Decode_ToSystemTextSerializerDecoratedType_Should_UseDecoratedName_Bug456() + { + var serializer = new SystemTextSerializer(); + var alg = new NoneAlgorithm(); + + var expected = new TestData.TestDataSystemTextSerializerDecorated + { + City = "Amsterdam", + }; + + var encoded = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer) + .Encode(expected); + encoded.Should().NotBeNullOrEmpty(); + + var actual = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer) + .Decode(encoded); + actual.Should().BeEquivalentTo(expected); + } + + [TestMethod] + public void Encode_Decode_Should_Return_Token_Nested_Data_ShouldRespectAttributes_SystemTextSerializer() + { + var serializer = new SystemTextSerializer(); + var alg = new NoneAlgorithm(); + + var expected = new TestData.TestDataSystemTextSerializerDecorated + { + City = "Amsterdam", + }; + + var encoded = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer) + .AddClaim("Data", expected) + .Encode(); + encoded.Should().NotBeNullOrEmpty(); + + var actual = JwtBuilder.Create() + .WithAlgorithm(alg) + .WithJsonSerializer(serializer) + .WithJsonSerializer(serializer) + .Decode(encoded); + actual.Data.Should().BeEquivalentTo(expected); + } +#endif } } diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index 2dd229f23..e26a3d804 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs @@ -230,78 +230,46 @@ public void Encode_Should_Return_Token_With_Custom_Extra_Headers_Full_Payload() } [TestMethod] - public void Encode_Should_Return_Token_With_Custom_Extra_Headers_Full_Payload_And_Claims() + public void Encode_Should_Throw_NotSupportedException_When_Using_EncodePayload_WithAddClaim() { const string key = TestData.Secret; - var token = JwtBuilder.Create() - .WithAlgorithm(TestData.HMACSHA256Algorithm) - .WithSecret(key) - .AddHeader("version", 1) - .AddClaim("ExtraClaim", "ValueClaim") - .Encode(TestData.Customer); - - token.Should() - .Be(TestData.TokenWithCustomTypeHeader3AndClaim, "because the same data encoded with the same key must result in the same token"); - } - - [TestMethod] - public void Encode_Should_Return_Token_With_Custom_Extra_Headers_Full_Payload_And_Claims_With_Nested() - { - const string key = TestData.Secret; - - var token = JwtBuilder.Create() - .WithAlgorithm(TestData.HMACSHA256Algorithm) - .WithSecret(key) - .AddHeader("version", 1) - .AddClaim("ExtraClaim", new { NestedProperty1 = "Foo", NestedProperty2 = 3 }) - .Encode(TestData.Customer); - - token.Should() - .Be(TestData.TokenWithCustomTypeHeader3AndClaimNested, "because the same data encoded with the same key must result in the same token"); - } - - [TestMethod] - public void Encode_Should_Return_Token_With_Custom_Extra_Headers_Full_Payload_And_Claims_With_Nested_TypesMatch() - { - const string key = TestData.Secret; - - var token = JwtBuilder.Create() - .WithAlgorithm(TestData.HMACSHA256Algorithm) - .WithSecret(key) - .AddHeader("version", 1) - .AddClaim("ExtraClaim", new { NestedProperty1 = "Foo", NestedProperty2 = 3 }) - .Encode(typeof(Customer), TestData.Customer); + Action action = () => + JwtBuilder.Create() + .WithAlgorithm(TestData.HMACSHA256Algorithm) + .WithSecret(key) + .AddHeader("version", 1) + .AddClaim("ExtraClaim", "ValueClaim") + .Encode(TestData.Customer); - token.Should() - .Be(TestData.TokenWithCustomTypeHeader3AndClaimNested, "because the same data encoded with the same key must result in the same token"); + action.Should() + .Throw("because using both Encode(payload) and AddClaim is not supported"); } [TestMethod] - public void Encode_Should_Return_ThrowTargetException_Encode_TypesMatch() + public void Encode_Should_Throw_NotSupportedException_When_Using_EncodePayload_WithAddClaims() { const string key = TestData.Secret; Action action = () => + { + var claims = new Dictionary + { + { "key" , "value" } + }; + JwtBuilder.Create() .WithAlgorithm(TestData.HMACSHA256Algorithm) .WithSecret(key) .AddHeader("version", 1) - .AddClaim("ExtraClaim", new { NestedProperty1 = "Foo", NestedProperty2 = 3 }) - .Encode(typeof(string), TestData.Customer); + .AddClaims(claims) + .Encode(TestData.Customer); + }; - if (IsRunningOnMono()) - { - action.Should() - .Throw("Exception has been thrown by the target of an invocation."); - } - else - { - action.Should() - .Throw("Object does not match target type."); - } + action.Should() + .Throw("because using both Encode(payload) and AddClaims() is not supported"); } - + /// /// Copied from: https://stackoverflow.com/a/7077620/2890855 /// @@ -382,6 +350,35 @@ public void Encode_With_Secret_Should_Return_Valid_Token_Using_Json_Net() } #if NETSTANDARD2_0 || NET6_0_OR_GREATER + + [TestMethod] + public void Encode_Should_Return_Token_With_Custom_Extra_Headers_Full_Payload_SystemTextSerializer() + { + var serializer = new SystemTextSerializer(); + + const string key = TestData.Secret; + + var token = JwtBuilder.Create() + .WithAlgorithm(TestData.HMACSHA256Algorithm) + .WithSecret(key) + .WithJsonSerializer(serializer) + .AddHeader("version", 1) + .Encode( + new + { + ExtraClaim = new + { + NestedProperty1 = "Foo", + NestedProperty2 = 3 + }, + FirstName = "Jesus", + Age = 33 + }); + + token.Should() + .Be(TestData.TokenWithCustomTypeHeader3AndClaimNested, "because the same data encoded with the same key must result in the same token"); + } + [TestMethod] public void Encode_Test_Bug438() { diff --git a/tests/JWT.Tests.Common/Models/TestData.cs b/tests/JWT.Tests.Common/Models/TestData.cs index 94da079e8..3db70255c 100644 --- a/tests/JWT.Tests.Common/Models/TestData.cs +++ b/tests/JWT.Tests.Common/Models/TestData.cs @@ -3,7 +3,7 @@ using System.Security.Cryptography.X509Certificates; using JWT.Algorithms; -#if NETSTANDARD2_1 || NET6_0 +#if NETSTANDARD2_1 || NET6_0_OR_GREATER using System.Security.Cryptography; #endif @@ -17,6 +17,30 @@ public static class TestData Age = 33 }; + public class PayloadWithNestedJsonNetData + { + public TestDataJsonNetDecorated Data { get; set; } + } + + public class TestDataJsonNetDecorated + { + [Newtonsoft.Json.JsonProperty("AT")] + public string City { get; set; } + } + +#if NETSTANDARD2_0 || NET6_0 + public class PayloadWithNestedSystemTextSerializerData + { + public TestDataSystemTextSerializerDecorated Data { get; set; } + } + + public class TestDataSystemTextSerializerDecorated + { + [System.Text.Json.Serialization.JsonPropertyName("AT")] + public string City { get; set; } + } +#endif + public const string Secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk"; public const string Secret2 = "QWORIJkmQWEDIHbjhOIHAUSDFOYnUGWEYT"; @@ -68,7 +92,7 @@ public static class TestData public static readonly X509Certificate2 CertificateWithPublicKeyEcdsa = new X509Certificate2( Convert.FromBase64String(ServerPublicKeyEcdsa)); -#if NETSTANDARD2_1 || NET6_0 +#if NETSTANDARD2_1 || NET6_0_OR_GREATER public static readonly X509Certificate2 CertificateWithPrivateKey = CreateCertificate(); private static X509Certificate2 CreateCertificate() @@ -83,4 +107,4 @@ private static X509Certificate2 CreateCertificate() } #endif } -} \ No newline at end of file +}