From 341da1cb0c06162a3a67099cc0ff7b3f67e5e786 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Wed, 25 Jan 2023 23:45:21 +0100 Subject: [PATCH] WIP JwtEncoder.Encode() now accepts a list of payloads so we can use both claims and payload --- src/JWT/Builder/JwtBuilder.cs | 51 +++---------------- src/JWT/IJsonSerializer.cs | 8 +++ src/JWT/IJwtEncoder.cs | 2 + src/JWT/JwtEncoder.cs | 41 +++++++++++++++ src/JWT/Serializers/JsonNetSerializer.cs | 10 ++++ src/JWT/Serializers/SystemTextSerializer.cs | 28 +++++++++- .../Builder/JwtBuilderEncodeTests.cs | 19 +------ 7 files changed, 96 insertions(+), 63 deletions(-) diff --git a/src/JWT/Builder/JwtBuilder.cs b/src/JWT/Builder/JwtBuilder.cs index 08c46b8df..92caafc12 100644 --- a/src/JWT/Builder/JwtBuilder.cs +++ b/src/JWT/Builder/JwtBuilder.cs @@ -278,18 +278,16 @@ public string Encode(Type payloadType, object payload) 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 => GetPropName(prop), prop => prop.GetValue(payload, null)); - - foreach (var pair in dic) + // Preserve old behaviour checking the type, is it needed? + if (payloadType != payload.GetType()) { - _jwt.Payload.Add(pair.Key, pair.Value); + throw new NotSupportedException("Object does not match target type."); } + + EnsureCanEncode(); - return Encode(); + return _encoder.Encode(_jwt.Header, new[] {_jwt.Payload, payload}, _secrets?[0]); } /// @@ -482,42 +480,5 @@ 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/IJsonSerializer.cs b/src/JWT/IJsonSerializer.cs index 12007ff5c..0ebbefe6d 100644 --- a/src/JWT/IJsonSerializer.cs +++ b/src/JWT/IJsonSerializer.cs @@ -21,6 +21,14 @@ public interface IJsonSerializer /// The JSON string deserialize. /// Strongly-typed object. object Deserialize(Type type, string json); + + /// + /// Merges two objects into a single JSON string. + /// + /// The first object to serialize + /// The second object to serialize, note that any existing property in obj1 will be + /// JSON string + string MergeObjects(object obj1, object obj2); } /// diff --git a/src/JWT/IJwtEncoder.cs b/src/JWT/IJwtEncoder.cs index e37835c59..492192939 100644 --- a/src/JWT/IJwtEncoder.cs +++ b/src/JWT/IJwtEncoder.cs @@ -18,6 +18,8 @@ public interface IJwtEncoder /// The key bytes used to sign the token /// The generated JWT string Encode(IDictionary extraHeaders, object payload, byte[] key); + + string Encode(IDictionary extraHeaders, object[] payloads, byte[] key); } /// diff --git a/src/JWT/JwtEncoder.cs b/src/JWT/JwtEncoder.cs index 126e9f7c1..02dd0716c 100644 --- a/src/JWT/JwtEncoder.cs +++ b/src/JWT/JwtEncoder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using JWT.Algorithms; using static JWT.Internal.EncodingHelper; @@ -39,6 +40,46 @@ public JwtEncoder(IJwtAlgorithm algorithm, IJsonSerializer jsonSerializer, IBase { } + public string Encode(IDictionary extraHeaders, object[] payloads, byte[] key) + { + if (payloads is null) + throw new ArgumentNullException(nameof(payloads)); + + if (!payloads.Any() || payloads.Length != 2) + { + throw new NotSupportedException("Only two payloads are supported"); + } + + var algorithm = _algFactory.Create(null); + if (algorithm is null) + throw new ArgumentNullException(nameof(algorithm)); + if (!algorithm.IsAsymmetric() && key is null && algorithm is not NoneAlgorithm) + throw new ArgumentNullException(nameof(key)); + + var header = extraHeaders is null ? + new Dictionary(2, StringComparer.OrdinalIgnoreCase) : + new Dictionary(extraHeaders, StringComparer.OrdinalIgnoreCase); + + if (!header.ContainsKey("typ")) + { + header.Add("typ", "JWT"); + } + header.Add("alg", algorithm.Name); + + var headerBytes = GetBytes(_jsonSerializer.Serialize(header)); + + var payloadBytes = GetBytes(_jsonSerializer.MergeObjects(payloads[0], payloads[1])); + + var headerSegment = _urlEncoder.Encode(headerBytes); + var payloadSegment = _urlEncoder.Encode(payloadBytes); + + var stringToSign = headerSegment + "." + payloadSegment; + var bytesToSign = GetBytes(stringToSign); + + var signatureSegment = GetSignatureSegment(algorithm, key, bytesToSign); + return stringToSign + "." + signatureSegment; + } + /// /// public string Encode(IDictionary extraHeaders, object payload, byte[] key) diff --git a/src/JWT/Serializers/JsonNetSerializer.cs b/src/JWT/Serializers/JsonNetSerializer.cs index 4dbb02115..2190417d7 100644 --- a/src/JWT/Serializers/JsonNetSerializer.cs +++ b/src/JWT/Serializers/JsonNetSerializer.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace JWT.Serializers { @@ -58,5 +59,14 @@ public object Deserialize(Type type, string json) using var jsonReader = new JsonTextReader(stringReader); return _serializer.Deserialize(jsonReader, type); } + + public string MergeObjects(object obj1, object obj2) + { + var jObject1 = JObject.Parse(Serialize(obj1)); + var jObject2 = JObject.Parse(Serialize(obj2)); + + jObject1.Merge(jObject2); + return jObject1.ToString(Formatting.None); + } } } diff --git a/src/JWT/Serializers/SystemTextSerializer.cs b/src/JWT/Serializers/SystemTextSerializer.cs index 84d9849d0..9461dc1af 100644 --- a/src/JWT/Serializers/SystemTextSerializer.cs +++ b/src/JWT/Serializers/SystemTextSerializer.cs @@ -1,6 +1,8 @@ #if MODERN_DOTNET using System; +using System.Linq; using System.Text.Json; +using System.Text.Json.Nodes; using JWT.Serializers.Converters; namespace JWT.Serializers @@ -19,7 +21,7 @@ public class SystemTextSerializer : IJsonSerializer new DictionaryStringObjectJsonConverterCustomWrite() } }; - + /// /// public string Serialize(object obj) @@ -43,6 +45,30 @@ public object Deserialize(Type type, string json) return JsonSerializer.Deserialize(json, type, _optionsForDeserialize); } + + public string MergeObjects(object obj1, object obj2) + { + if (obj1 == null) + { + throw new ArgumentNullException(nameof(obj1)); + } + + if (obj2 == null) + { + throw new ArgumentNullException(nameof(obj2)); + } + + var jsonNode1 = JsonNode.Parse(Serialize(obj1)); + var jsonNode2 = JsonNode.Parse(Serialize(obj2)); + + foreach (var property in jsonNode2.AsObject().ToArray()) + { + jsonNode2.AsObject().Remove(property.Key); + jsonNode1[property.Key] = property.Value; + } + + return jsonNode1.ToJsonString(_optionsForSerialize); + } } } #endif diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index 9a64854b7..85995a40d 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs @@ -290,25 +290,10 @@ public void Encode_Should_Return_ThrowTargetException_Encode_TypesMatch() .AddClaim("ExtraClaim", new { NestedProperty1 = "Foo", NestedProperty2 = 3 }) .Encode(typeof(string), 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("Object does not match target type."); } - /// - /// Copied from: https://stackoverflow.com/a/7077620/2890855 - /// - /// - private static bool IsRunningOnMono() => - Type.GetType("Mono.Runtime") is not null; - [TestMethod] public void Encode_Should_Return_Token_With_Custom_Extra_Headers_Full_Payload2() {