jwt-dotnet/jwt

Not properly serilized object in fluent version of Decode

master0luc opened this issue · 9 comments

Net7[EDITED], JWT10.0.1 [EDITED]
Despite this information https://github.com/jwt-dotnet/jwt#parsing-decoding-and-verifying-token
Two types of encoding do not produce the same result ():

        static TAuthorizationTicket Parse1<TAuthorizationTicket>(string value, string key)
            => JwtBuilder.Create()
                .WithAlgorithm(new HMACSHA256Algorithm())
                .WithSecret(key)
                .MustVerifySignature()
                .Decode<TAuthorizationTicket>(value);
        static TAuthorizationTicket Parse2<TAuthorizationTicket>(string token, string key)
        {
            IJsonSerializer serializer = new JsonNetSerializer();
            IDateTimeProvider provider = new UtcDateTimeProvider();
            IJwtValidator validator = new JwtValidator(serializer, provider);
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
            IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
            UTF8Encoding _utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
            var obj = decoder.DecodeToObject<TAuthorizationTicket>(token, _utf8Encoding.GetBytes(key), true);
            return obj;
        }

Parse1 produces the default token without origin data while Parse2 does correct object.
Test method for reproducing:

using Xunit;
using System;
using FluentAssertions;
using Newtonsoft.Json;
using System.Collections.Generic;
using JWT.Algorithms;
using JWT.Builder;
using JWT.Serializers;
using JWT;
using System.Text;

namespace Tests
{
   public class TokenTests
   {
       [Fact]
       public void TestTokenParser1()
       {
           var key = "test";
           string token = GetToken(key);

           var parsedToken = Parse<AuthorizationTicket>(token, key);
           parsedToken.Should().NotBeNull();
           var res = parsedToken.Should().BeOfType<AuthorizationTicket>();
           res.Which.IsAuthenticated.Should().BeTrue();
           res.Which.Permits.Should().HaveCount(n => n == 2);
           res.Which.User.Should().NotBeNull();
           res.Which.User.Should().BeOfType<KreoUser>().Which.Email.Should().Be("user@domain.ext");
           res.Which.Company.Should().NotBeNull();
           res.Which.Company.Should().BeOfType<KreoCompany>().Which.Id.Should().Be(-333);
           res.Which.Company.Should().BeOfType<KreoCompany>().Which.OwnCompany.Should().BeTrue();
       }

       [Fact]
       public void TestTokenParser2()
       {
           var key = "test";
           string token = GetToken(key);

           var parsedToken = Parse2<AuthorizationTicket>(token, key);
           parsedToken.Should().NotBeNull();
           var res = parsedToken.Should().BeOfType<AuthorizationTicket>();
           res.Which.IsAuthenticated.Should().BeTrue();
           res.Which.Permits.Should().HaveCount(n => n == 2);
           res.Which.User.Should().NotBeNull();
           res.Which.User.Should().BeOfType<KreoUser>().Which.Email.Should().Be("user@domain.ext");
           res.Which.Company.Should().NotBeNull();
           res.Which.Company.Should().BeOfType<KreoCompany>().Which.Id.Should().Be(-333);
           res.Which.Company.Should().BeOfType<KreoCompany>().Which.OwnCompany.Should().BeTrue();
       }

       private static string GetToken(string key)
       {
           AuthorizationTicket ticket = new AuthorizationTicket();
           ticket.IsAuthenticated = true;
           ticket.Company = new KreoCompany() { OwnCompany = true, Id = -333 };
           ticket.User = new KreoUser() { Email = "user@domain.ext" };
           ticket.Permits = new List<string> { "one", "two" };

           var token = JwtBuilder.Create()
               .WithAlgorithm(new HMACSHA256Algorithm())
               .WithSecret(key)
               .AddClaim("is_authenticated", ticket.IsAuthenticated)
               .AddClaim("company", ticket.Company)
               .AddClaim("user", ticket.User)
               .AddClaim("permits", ticket.Permits)
               .Encode();
           return token;
       }
       static TAuthorizationTicket Parse<TAuthorizationTicket>(string value, string key)
           => JwtBuilder.Create()
               .WithAlgorithm(new HMACSHA256Algorithm())
               .WithSecret(key)
               .MustVerifySignature()
               .Decode<TAuthorizationTicket>(value);
       static TAuthorizationTicket Parse2<TAuthorizationTicket>(string token, string key)
       {
           IJsonSerializer serializer = new JsonNetSerializer();
           IDateTimeProvider provider = new UtcDateTimeProvider();
           IJwtValidator validator = new JwtValidator(serializer, provider);
           IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
           IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
           IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
           UTF8Encoding _utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
           var obj = decoder.DecodeToObject<TAuthorizationTicket>(token, _utf8Encoding.GetBytes(key), true);
           return obj;
       }
       class AuthorizationTicket
       {
           [JsonProperty("is_authenticated")]
           public bool IsAuthenticated { get; set; }

           [JsonProperty("user")]
           public KreoUser User { get; set; }
               = new KreoUser();

           [JsonProperty("company")]
           public KreoCompany Company { get; set; }
               = new KreoCompany();

           [JsonProperty("permits")]
           public List<string> Permits { get; set; }
               = new List<string>();
       }
       class KreoUser
       {
           public Guid Id { get; set; }
           public string Email { get; set; }
           public string FirstName { get; set; }
           public string LastName { get; set; }
           public string FullName { get; set; }
           public bool IsAdmin { get; set; }
           public bool EmailVerified { get; set; }//todo
           public List<string> Groups { get; set; }
       }
       class KreoCompany
       {
           public bool? OwnCompany { get; set; }
           public long? Id { get; set; }
           public string SubscriptionType { get; set; }
       }
   }
}

Hi,
Can you please provide this string outputs? How's the correct and incorrect look like?

Hi, all data is in test method:

  1. create AuthorizationTicket ,
  2. encode AuthorizationTicket to token string:
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc19hdXRoZW50aWNhdGVkIjp0cnVlLCJjb21wYW55Ijp7Ik93bkNvbXBhbnkiOnRydWUsIklkIjotMzMzLCJTdWJzY3JpcHRpb25UeXBlIjpudWxsfSwidXNlciI6eyJJZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsIkVtYWlsIjoidXNlckBkb21haW4uZXh0IiwiRmlyc3ROYW1lIjpudWxsLCJMYXN0TmFtZSI6bnVsbCwiRnVsbE5hbWUiOm51bGwsIklzQWRtaW4iOmZhbHNlLCJFbWFpbFZlcmlmaWVkIjpmYWxzZSwiR3JvdXBzIjpudWxsfSwicGVybWl0cyI6WyJvbmUiLCJ0d28iXX0.EJfPFhSxfhw7y2-mDKJN1iog2IUqwEcA9gVuf55oXT0"
  1. try to parse this token:
    Parse(token, key) != Parse2(token, key);
    Parse(token, key) get this result:
    Bug1
    Parse2(token, key):
    bug2
    origin ticket looks like:
    bug0

What's your version? I release a fix to the latest version - 10.0.1. Using it (or latest main). the test succeeds.

Can you please try updating and let me know?

Hi,

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="JWT" Version="10.0.1" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
    <PackageReference Include="AutoFixture" Version="4.17.0" />
    <PackageReference Include="FluentAssertions" Version="6.8.0" />
    <PackageReference Include="Moq" Version="4.18.3" />
    <PackageReference Include="xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.console" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
    <DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
  </ItemGroup>

</Project>

simplified test file

using Xunit;
using System;
using FluentAssertions;
using Newtonsoft.Json;
using System.Collections.Generic;
using JWT.Algorithms;
using JWT.Builder;
using JWT.Serializers;
using JWT;
using System.Text;

namespace Tests
{
    public class TokenTests
    {
        [Fact]
        public void TestTokenParser1()
        {
            var ticket = GetAuthorizationTicket();
            var key = "test";
            string token = GetToken(ticket, key);

            var parsedToken = Parse<AuthorizationTicket>(token, key);
            parsedToken.Should().BeEquivalentTo<AuthorizationTicket>(ticket);

        }

        [Fact]
        public void TestTokenParser2()
        {
            var ticket = GetAuthorizationTicket();
            var key = "test";
            string token = GetToken(ticket, key);

            var parsedToken = Parse2<AuthorizationTicket>(token, key);
            parsedToken.Should().BeEquivalentTo<AuthorizationTicket>(ticket);

        }

        private static string GetToken(AuthorizationTicket ticket, string key)
        {
            var token = JwtBuilder.Create()
                .WithAlgorithm(new HMACSHA256Algorithm())
                .WithSecret(key)
                .AddClaim("is_authenticated", ticket.IsAuthenticated)
                .AddClaim("company", ticket.Company)
                .AddClaim("user", ticket.User)
                .AddClaim("permits", ticket.Permits)
                .Encode();
            return token;
        }

        private static AuthorizationTicket GetAuthorizationTicket()
        {
            AuthorizationTicket ticket = new AuthorizationTicket();
            ticket.IsAuthenticated = true;
            ticket.Company = new Company() { OwnCompany = true, Id = -333 };
            ticket.User = new User() { Email = "user@domain.ext" };
            ticket.Permits = new List<string> { "one", "two" };
            return ticket;
        }

        static TAuthorizationTicket Parse<TAuthorizationTicket>(string value, string key)
            => JwtBuilder.Create()
                .WithAlgorithm(new HMACSHA256Algorithm())
                .WithSecret(key)
                .MustVerifySignature()
                .Decode<TAuthorizationTicket>(value);
        static TAuthorizationTicket Parse2<TAuthorizationTicket>(string token, string key)
        {
            IJsonSerializer serializer = new JsonNetSerializer();
            IDateTimeProvider provider = new UtcDateTimeProvider();
            IJwtValidator validator = new JwtValidator(serializer, provider);
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
            IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
            UTF8Encoding _utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
            var obj = decoder.DecodeToObject<TAuthorizationTicket>(token, _utf8Encoding.GetBytes(key), true);
            return obj;
        }
        class AuthorizationTicket
        {
            [JsonProperty("is_authenticated")]
            public bool IsAuthenticated { get; set; }

            [JsonProperty("user")]
            public User User { get; set; }
                = new User();

            [JsonProperty("company")]
            public Company Company { get; set; }
                = new Company();

            [JsonProperty("permits")]
            public List<string> Permits { get; set; }
                = new List<string>();
        }
        class User
        {
            public Guid Id { get; set; }
            public string Email { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public string FullName { get; set; }
            public bool IsAdmin { get; set; }
            public bool EmailVerified { get; set; }//todo
            public List<string> Groups { get; set; }
        }
        class Company
        {
            public bool? OwnCompany { get; set; }
            public long? Id { get; set; }
            public string SubscriptionType { get; set; }
        }
    }
}

TestTokenParser1 => failed to restore original object
TestTokenParser2 => it is okey

Sorry for the delay in response, I'll take a look!

I think this is related to #456 as you're also using attributes for the payload. Try the fix in PR #462

Hi, JWT.10.0.2-beta1 does not solve mention above problem, but I looked through #456
and figured out that if I chained ".WithJsonSerializer(new JsonNetSerializer())" before ".Decode", so all will work fine.

        static TAuthorizationTicket Parse<TAuthorizationTicket>(string value, string key)
            => JwtBuilder.Create()
                .WithAlgorithm(new HMACSHA256Algorithm())
                .WithSecret(key)
                .MustVerifySignature()
                //added next line solve a serialization problem
                .WithJsonSerializer(new JsonNetSerializer())
                .Decode<TAuthorizationTicket>(value);

I hope, it would help you to fix this behaviour

I have looked more closely to your issue and it seems that you are using the wrong json attribute to decorate your payload.

You're using JsonProperty on the class AuthorizationTicket, that one is for Json.Net (Newtonsoft.Json), so that's why you get it working by adding your line "WithJsonSerializer(new JsonNetSerializer())"

If you want to use the default System.Text.Json serializer you should use the attribute JsonPropertyName to decorate your class.

One quick way to make sure you don't mix serializers is to search for any using of Newtonsoft.Json and remove them.

PS. You could make your code more reliant against typos if you read the attribute name of the property.

Consider this extension method:

public static class TypeExtensions
{
    public static string GetJsonName(this Type type, string propertyName)
    {
        var attributes = type.GetProperty(propertyName)?.GetCustomAttributes(inherit: false);

        if (attributes == null)
        {
            return propertyName;
        }
        
        foreach (var attribute in attributes)
        {
            if (attribute is System.Text.Json.Serialization.JsonPropertyNameAttribute stjProperty)
            {
                return stjProperty.Name;
            }
        }

        return propertyName;
    }
}

Then you can add the claims by this way:

.AddClaim(typeof(AuthorizationTicket).GetJsonName(nameof(AuthorizationTicket.IsAuthenticated), ticket.IsAuthenticated)

But if you have the whole object you can just encode the jwt like this:

var token = JwtBuilder.Create()
                      .WithAlgorithm(new HMACSHA256Algorithm())
                      .WithSecret(key)
                      .Encode(ticket);

Hi @hartmark,
You are not right, if wanted to use System.Text.Json, I would.
After some time mulling over my case, your answer hits me:
JWT 10.. has a breaking change which has been mentioned briefly:
shifting in default serializer: from Newtonsoft.Json to System.Text.Json and what does mean!
Your advice "One quick way to make sure you don't mix serializers is to search for any using of Newtonsoft.Json and remove them." seems strange but ok, can be, but, more convinent, mention it in readme.md like a breaking change, IMHO

In addition, I can say, that your "TypeExtensions" would not work with Newtonsoft.Json decorator either, obviously.

In my, mentioned above. case, there is one flaw: explicit setting name of Claims:

 .AddClaim("is_authenticated", ticket.IsAuthenticated)

which need additional effort in decode pipeline

.WithJsonSerializer(new JsonNetSerializer())

because names are taken from Attributes.

So case is closed.
Thank You for your attention and time