jwt icon indicating copy to clipboard operation
jwt copied to clipboard

JwtBuilder throws ArgumentNullException when Encode called and IAlgorithmFactory is set

Open MaxLevs opened this issue 3 years ago • 14 comments

Bug description

I've tried to use JwtBuilder with ECDsa keys. For this purpose I've registered IAlgorithmFactory as singletone with this:

builder.Services.AddSingleton<IAlgorithmFactory>(services =>
{
    var securityOptions = services.GetRequiredService<IConfiguration>().GetSection("Security");

    var privateKeyTask = LoadEcKeyAsync(LoadFileFromConfigAsync(securityOptions, "JwtPrivateKeyFilePath"));
    var publicKeyTask = LoadEcKeyAsync(LoadFileFromConfigAsync(securityOptions, "JwtPublicKeyFilePath"));

    Task.WaitAll(privateKeyTask, publicKeyTask);

    return new ECDSAAlgorithmFactory(publicKeyTask.Result, privateKeyTask.Result);
});

In my AuthService I've got IAlgorithmFactory via DI and called the method which creates tokens

    public AuthService(ILogger<AuthService> logger, IAlgorithmFactory algorithmFactory)
    {
        _logger = logger;
        _algorithmFactory = algorithmFactory;
    }

    public SessionResult CreateSession(User user)
    {
        var sessionId = Guid.NewGuid();
        var now = DateTime.UtcNow;

        var accessToken = JwtBuilder.Create()
            .WithAlgorithmFactory(_algorithmFactory)
            .AddClaim("session_id", sessionId)
            .Issuer("me")
            .Audience("you")
            .IssuedAt(now)
            .ExpirationTime(now.AddMinutes(30))
            .Encode();

        return new SessionResult(sessionId, accessToken);
    }

Expected behaviour: the method returns instance of SessionResult with tokens. Actual behaviour: ArgumentNullException is throws after Encode() method is called.

Bug reason

When JwtBuilder.Encode() method invokes, it invokes method JwtEncoder.Encode(IDictionary<string, object> extraHeaders, object payload, byte[] key) which creates instance of IJwtAlgorithm with this line

// JwtEncoder.cs, line 49
var algorithm = _algFactory.Create(null);

But this provokes a call of the JwtAlgorithmFactory.Create(JwtDecoderContext context) method which contains this line

// JwtAlgorithmFactory.cs, line 11
var algorithm = context?.Header?.Algorithm ?? throw new ArgumentNullException(nameof(context));

MaxLevs avatar Sep 21 '22 18:09 MaxLevs

Hi! Thanks for reporting this issue. Can you please try the latest beta version? Let me know if it still behaves this way.

abatishchev avatar Sep 21 '22 18:09 abatishchev

Or I might know what's the issue, it in the poor design: an instance of algorithm factory suitable for encoding is not suitable for decoding. And that's exactly the issue you've hit: in order to decode, it reads the algorithm from the context. But in order to encode, user needs to specify the algorithm explicitly.

abatishchev avatar Sep 21 '22 18:09 abatishchev

In other words, what's _algorithmFactory which you passed at WithAlgorithmFactory(_algorithmFactory)?

abatishchev avatar Sep 21 '22 18:09 abatishchev

In other words, what's _algorithmFactory which you passed at WithAlgorithmFactory(_algorithmFactory)?

The first block of code in this issue. I've created instance of ECDSAAlgorithmFactory in DI.

MaxLevs avatar Sep 21 '22 18:09 MaxLevs

Hi! Thanks for reporting this issue. Can you please try the latest beta version? Let me know if it still behaves this way.

The latest beta (10.0.0-beta4) doesn't solve the problem

MaxLevs avatar Sep 21 '22 18:09 MaxLevs

Ah, sorry, missed that part. Looking...

abatishchev avatar Sep 21 '22 18:09 abatishchev

First, as a workaround, what you can do is to register a DelegateAlgorithmFactory instead:

return new DelegateAlgorithmFactory(
  new ECDSAAlgorithm(publicKeyTask.Result, privateKeyTask.Result));

I'm looking for a better option.

abatishchev avatar Sep 21 '22 18:09 abatishchev

@MaxLevs to make the code little nicer, I've made a change in #437. Do you mind consuming the packages produced from its build to see if works for you?

abatishchev avatar Sep 21 '22 19:09 abatishchev

@MaxLevs to make the code little nicer, I've made a change in #437. Do you mind consuming the packages produced from its build to see if works for you?

Can you somehow increment version? I can't install this local version of package instead of one in nuget.org. Or updated version is in nuget.org already and I can just clear cache and restore?

MaxLevs avatar Sep 21 '22 20:09 MaxLevs

First, as a workaround, what you can do is to register a DelegateAlgorithmFactory instead:

return new DelegateAlgorithmFactory(
  new ECDSAAlgorithm(publicKeyTask.Result, privateKeyTask.Result));

ECDSAAlgorithm constructor is protected

MaxLevs avatar Sep 21 '22 20:09 MaxLevs

@MaxLevs to make the code little nicer, I've made a change in #437. Do you mind consuming the packages produced from its build to see if works for you?

Okay. I've created local folder source and put this *.nupkg files into it. Then cleared cache and try to run code. Behaviour is not changed. But I'm not sure that nuget restored package from local source.

MaxLevs avatar Sep 21 '22 20:09 MaxLevs

Yes, I've bumped the version to 10.0.0-beta9. If you drop the package to a "local feed" (folder), it'll be picked up.

The change should improve the composition of the factories but you still need a DelegateAlgorithmFactory:

return new DelegateAlgorithmFactory(
  new new ECDSAAlgorithmFactory(publicKeyTask.Result, privateKeyTask.Result));

abatishchev avatar Sep 21 '22 20:09 abatishchev

Okay. First of all, I've cloned branch delegate-factory-2 to my project and added as dependency.

<ItemGroup>
  <ProjectReference Include="..\jwt\src\JWT.Extensions.AspNetCore\JWT.Extensions.AspNetCore.csproj" />
</ItemGroup>

Second of all, I've added Factory to DI this way.

return new DelegateAlgorithmFactory(
  new new ECDSAAlgorithmFactory(publicKeyTask.Result, privateKeyTask.Result));

Exception

[ERROR] An unhandled exception has occurred while executing the request.
EXCEPTION: System.ArgumentNullException: Value cannot be null. (Parameter 'context')
   at JWT.Algorithms.JwtAlgorithmFactory.Create(JwtDecoderContext context) in /jwt/src/JWT/Algorithms/JwtAlgorithmFactory.cs:line 11
   at JWT.Algorithms.DelegateAlgorithmFactory.<>c__DisplayClass3_0.<.ctor>b__0(JwtDecoderContext c) in /jwt/src/JWT/Algorithms/DelegateAlgorithmFactory.cs:line 35
   at JWT.Algorithms.DelegateAlgorithmFactory.Create(JwtDecoderContext context) in /jwt/src/JWT/Algorithms/DelegateAlgorithmFactory.cs:line 54
   at JWT.JwtEncoder.Encode(IDictionary`2 extraHeaders, Object payload, Byte[] key) in /jwt/src/JWT/JwtEncoder.cs:line 49
   at JWT.Builder.JwtBuilder.Encode() in /jwt/src/JWT/Builder/JwtBuilder.cs:line 263

MaxLevs avatar Sep 21 '22 21:09 MaxLevs

And now I can get last changes from repo directly.

But yeah, local source folder did work but git pull is easiest way.

MaxLevs avatar Sep 21 '22 21:09 MaxLevs

I'm looking further into the problem.

To overcome the issue of a protected ctor, construct of these types:

  • ES256Algorithm
  • ES384Algorithm
  • ES512Algorithm

abatishchev avatar Sep 21 '22 22:09 abatishchev

  • ES256Algorithm

I've created factory with this code


    var algo = new ES256Algorithm(publicKeyTask.Result, privateKeyTask.Result);

    return new DelegateAlgorithmFactory(context => algo);

And it still doesn't work.

[ERROR] An unhandled exception has occurred while executing the request.
EXCEPTION: System.Reflection.TargetParameterCountException: Parameter count mismatch.
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, Object[] index)
   at JWT.Serializers.Converters.DictionaryStringObjectJsonConverterCustomWrite.<>c__DisplayClass2_0.<HandleValue>b__1(PropertyInfo p) in /jwt/src/JWT/Serializers/Converters/DictionaryStringObjectJsonConverter.cs:line 121
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](TSource[] source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
   at JWT.Serializers.Converters.DictionaryStringObjectJsonConverterCustomWrite.HandleValue(Utf8JsonWriter writer, String key, Object objectValue) in /jwt/src/JWT/Serializers/Converters/DictionaryStringObjectJsonConverter.cs:line 119
   at JWT.Serializers.Converters.DictionaryStringObjectJsonConverterCustomWrite.Write(Utf8JsonWriter writer, Dictionary`2 value, JsonSerializerOptions options) in /jwt/src/JWT/Serializers/Converters/DictionaryStringObjectJsonConverter.cs:line 45
   at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.JsonSerializer.WriteUsingSerializer[TValue](Utf8JsonWriter writer, TValue& value, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.WriteStringUsingSerializer[TValue](TValue& value, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
   at JWT.Serializers.SystemTextSerializer.Serialize(Object obj) in /jwt/src/JWT/Serializers/SystemTextSerializer.cs:line 28
   at JWT.JwtEncoder.Encode(IDictionary`2 extraHeaders, Object payload, Byte[] key) in /jwt/src/JWT/JwtEncoder.cs:line 66
   at JWT.Builder.JwtBuilder.Encode() in /jwt/src/JWT/Builder/JwtBuilder.cs:line 263

MaxLevs avatar Sep 22 '22 06:09 MaxLevs

It's strange but when I've created new console project and include only Jwt.Net this code've worked.

await Task.WhenAll(publicKeyTask, privateKeyTask);

var algo = new ES256Algorithm(publicKeyTask.Result, privateKeyTask.Result);

var factory = new DelegateAlgorithmFactory(context => algo);

var now = DateTime.UtcNow;
var sessionId = Guid.NewGuid();

var baseJwtBuilder = JwtBuilder.Create()
    .WithAlgorithmFactory(factory)
    .AddClaim("session_id", sessionId.ToString())
    .Issuer("Security Guy")
    .Audience("Strict access perimeter")
    .IssuedAt(now)
    .ExpirationTime(now.AddMinutes(30));

var token = baseJwtBuilder.Encode();

Console.WriteLine($"Token: {token}");

But it throws the TargetParameterCountException if I do it throw AspNet.Core app

MaxLevs avatar Sep 22 '22 07:09 MaxLevs

What's the message and stack trace for TargetParameterCountException? Sounds like a different issue. Shall we close this one and start another?

abatishchev avatar Sep 22 '22 14:09 abatishchev

Yes. It's another one. I think we should start another issue with this.

Stacktrace

EXCEPTION: System.Reflection.TargetParameterCountException: Parameter count mismatch.
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, Object[] index)
   at JWT.Serializers.Converters.DictionaryStringObjectJsonConverterCustomWrite.<>c__DisplayClass2_0.<HandleValue>b__1(PropertyInfo p) in MyProject/jwt/src/JWT/Serializers/Converters/DictionaryStringObjectJsonConverter.cs:line 121
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](TSource[] source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
   at JWT.Serializers.Converters.DictionaryStringObjectJsonConverterCustomWrite.HandleValue(Utf8JsonWriter writer, String key, Object objectValue) in MyProject/jwt/src/JWT/Serializers/Converters/DictionaryStringObjectJsonConverter.cs:line 119
   at JWT.Serializers.Converters.DictionaryStringObjectJsonConverterCustomWrite.Write(Utf8JsonWriter writer, Dictionary`2 value, JsonSerializerOptions options) in MyProject/jwt/src/JWT/Serializers/Converters/DictionaryStringObjectJsonConverter.cs:line 45
   at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.JsonSerializer.WriteUsingSerializer[TValue](Utf8JsonWriter writer, TValue& value, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.WriteStringUsingSerializer[TValue](TValue& value, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
   at JWT.Serializers.SystemTextSerializer.Serialize(Object obj) in MyProject/jwt/src/JWT/Serializers/SystemTextSerializer.cs:line 28
   at JWT.JwtEncoder.Encode(IDictionary`2 extraHeaders, Object payload, Byte[] key) in MyProject/jwt/src/JWT/JwtEncoder.cs:line 66
   at JWT.Builder.JwtBuilder.Encode() in MyProject/jwt/src/JWT/Builder/JwtBuilder.cs:line 263
   at WebSockets.Services.AuthService.CreateSession(User user) in MyProject/DirtyChat/Services/AuthService.cs:line 92

MaxLevs avatar Sep 22 '22 17:09 MaxLevs

@MaxLevs see the continued discussion in #438

hartmark avatar Sep 22 '22 21:09 hartmark

Okay. I can create algorithm instance by myself to mock IAlgorithmFactory and it works. Its pretty nice workaround. But what's with original ECDSAAlgorithmFactory? Any plans to fix the bug?

@abatishchev @hartmark

MaxLevs avatar Sep 22 '22 22:09 MaxLevs