JwtBuilder throws ArgumentNullException when Encode called and IAlgorithmFactory is set
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));
Hi! Thanks for reporting this issue. Can you please try the latest beta version? Let me know if it still behaves this way.
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.
In other words, what's _algorithmFactory which you passed at WithAlgorithmFactory(_algorithmFactory)?
In other words, what's
_algorithmFactorywhich you passed atWithAlgorithmFactory(_algorithmFactory)?
The first block of code in this issue. I've created instance of ECDSAAlgorithmFactory in DI.
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
Ah, sorry, missed that part. Looking...
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.
@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?
@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?
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 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.
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));
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
And now I can get last changes from repo directly.
But yeah, local source folder did work but git pull is easiest way.
I'm looking further into the problem.
To overcome the issue of a protected ctor, construct of these types:
- ES256Algorithm
- ES384Algorithm
- ES512Algorithm
- 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
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
What's the message and stack trace for TargetParameterCountException? Sounds like a different issue. Shall we close this one and start another?
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 see the continued discussion in #438
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