Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not properly serilized object in fluent version of Decode #461

Closed
master0luc opened this issue Jan 18, 2023 · 9 comments
Closed

Not properly serilized object in fluent version of Decode #461

master0luc opened this issue Jan 18, 2023 · 9 comments
Assignees
Labels

Comments

@master0luc
Copy link

master0luc commented Jan 18, 2023

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("[email protected]");
           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("[email protected]");
           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 = "[email protected]" };
           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; }
       }
   }
}
@abatishchev abatishchev self-assigned this Jan 18, 2023
@abatishchev
Copy link
Member

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

@master0luc
Copy link
Author

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

@abatishchev
Copy link
Member

abatishchev commented Jan 18, 2023

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?

@master0luc
Copy link
Author

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 = "[email protected]" };
            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

@abatishchev
Copy link
Member

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

@abatishchev abatishchev added bug and removed question labels Jan 24, 2023
@hartmark
Copy link
Contributor

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

@master0luc
Copy link
Author

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

@hartmark
Copy link
Contributor

hartmark commented Jan 27, 2023

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);

@master0luc
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

3 participants