dvsekhvalnov / jose-jwt

Ultimate Javascript Object Signing and Encryption (JOSE), JSON Web Token (JWT) and Json Web Keys (JWK) Implementation for .NET and .NET Core
MIT License
936 stars 184 forks source link

Decode throws when a nested property of an encoded model is a System.Decimal with one or more decimal places (e.g., 24.00m) #229

Closed jonsagara closed 1 year ago

jonsagara commented 1 year ago
jose-jwt: 4.1.0
.NET SDK: 7.0.305

Algorithm: HS256

I ran into an issue when trying to add a nested object to the payload where one of the nested object properties is a System.Decimal. Here are the models:

/// <summary>
/// The main JWT payload model.
/// </summary>
public class SourceJwtModel
{
    public string jti { get; set; } = null!;
    public string sub { get; set; } = null!;
    public long iat { get; set; }

    public SourceJwtAddOnModel addOn { get; set; } = null!;
}

/// <summary>
/// A nested property on the source JWT payload model.
/// </summary>
public record SourceJwtAddOnModel(
    string name,
    decimal amount
    );

/// <summary>
/// The destination payload model. Think of the receiver of the JWT as residing in a totally different system.
/// </summary>
public class DestinationJwtModel
{
    public string jti { get; set; } = null!;
    public string sub { get; set; } = null!;
    public long iat { get; set; }
    public object addOn { get; set; } = null!;
}

If the amount property is a decimal value with no decimal places (e.g., 24m), then I can successfully encode and decode the JWT. Here is example data that encodes and decodes properly:

// Encoding/Decoding succeeds when amount is a System.Decimal with no decimal places.
var sourceJwtModel1 = new SourceJwtModel
{
    jti = "jti1",
    sub = "sub1",
    iat = 1688784499L,

    addOn = new SourceJwtAddOnModel(
        name: "500 widgets/year",
        amount: 24m // <-- no decimal places
        ),
};

However, as soon as amount contains one or more decimal places (e.g., 24.00m), JWT.Decode throws an exception. Here is an example data that encodes successfully, but throws upon decoding:

// Encoding succeeds, but Decoding fails when amount is a System.Decimal with 1 or more decimal places.
var sourceJwtModel2 = new SourceJwtModel
{
    jti = "jti2",
    sub = "sub2",
    iat = 1688784499L,

    addOn = new SourceJwtAddOnModel(
        name: "500 widgets/year",
        amount: 24.00m // <-- 2 decimal places
        ),
};

Here is the exception thrown by the JWT.Decode method:

System.Text.Json.JsonException: The JSON value could not be converted to System.Object. Path: $.amount | LineNumber: 0 | BytePositionInLine: 41.
 ---> System.FormatException: Either the JSON value is not in a supported format, or is out of bounds for an Int64.
   at System.Text.Json.ThrowHelper.ThrowFormatException(NumericType numericType)
   at System.Text.Json.Utf8JsonReader.GetInt64()
   at Jose.NestedDictionariesConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonDictionaryConverter`3.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TDictionary& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   --- End of inner exception stack trace ---
   at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, Utf8JsonReader& reader, Exception ex)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCoreAsObject(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](Utf8JsonReader& reader, JsonTypeInfo jsonTypeInfo, ReadStack& state)
   at System.Text.Json.JsonSerializer.Read[TValue](Utf8JsonReader& reader, JsonTypeInfo jsonTypeInfo)
   at Jose.NestedDictionariesConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Jose.JsonMapper.Parse[T](String json)
   at Jose.JWT.Decode[T](String token, Object key, JwsAlgorithm alg, JwtSettings settings)
   at JoseJwtReproduction.Program.EncodeAndDecodeJWT(SourceJwtModel sourceJwtModel) in C:\Dev\SANDBOX\JoseJwtReproduction\src\JoseJwtReproduction\Program.cs:line 50

Are System.Decimals not supported by the underlying serializer? Is there a workaround for this?

I have a full reproduction available here: https://github.com/jonsagara/JoseJwtReproduction

Thank you,

Jon

dvsekhvalnov commented 1 year ago

Hi @jonsagara , uhh, that's mostly question to System.Text.Json , i personally don't know every single detail on how it works with different data types, but i'd try next things:

  1. Try turn it to IDictionary: public object addOn { get; set; } = null!; -> public IDictionaty<string, object> addOn { get; set; } = null!;

  2. Try using different json mapper (e.g. Newtonsoft.Json), see here: https://github.com/dvsekhvalnov/jose-jwt#customizing-json---object-parsing--mapping

  3. After all you can avoid parsing your content to object model and simply string payload = JWT.Decode(...) to get raw payload back and do whatever you want with it after.

jonsagara commented 1 year ago

I tried the three options, and the winner seems to be option 3:

It worked when addOn was both an object and an IDictionary<string, object>.

I updated my sample project with the workaround.

Thanks, @dvsekhvalnov !