Skip to content

Commit

Permalink
add a new algorithm factory
Browse files Browse the repository at this point in the history
to create an algorithms based on JSON Web key sets
  • Loading branch information
CADBIMDeveloper committed Jan 27, 2024
1 parent c04de2b commit 96e1c76
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 11 deletions.
6 changes: 6 additions & 0 deletions src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JWT.Jwk;

public interface IJwtWebKeysCollectionFactory
{
JwtWebKeysCollection CreateKeys();
}
68 changes: 68 additions & 0 deletions src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Security.Cryptography;
using JWT.Algorithms;
using JWT.Exceptions;
using JWT.Serializers;

namespace JWT.Jwk
{
public sealed class JwtJsonWebKeySetAlgorithmFactory : IAlgorithmFactory
{
private readonly JwtWebKeysCollection _webKeysCollection;

public JwtJsonWebKeySetAlgorithmFactory(JwtWebKeysCollection webKeysCollection)
{
_webKeysCollection = webKeysCollection;
}

public JwtJsonWebKeySetAlgorithmFactory(Func<JwtWebKeysCollection> getJsonWebKeys)
{
_webKeysCollection = getJsonWebKeys();
}

public JwtJsonWebKeySetAlgorithmFactory(IJwtWebKeysCollectionFactory webKeysCollectionFactory)
{
_webKeysCollection = webKeysCollectionFactory.CreateKeys();
}

public JwtJsonWebKeySetAlgorithmFactory(string keySet, IJsonSerializer serializer)
{
_webKeysCollection = new JwtWebKeysCollection(keySet, serializer);
}

public JwtJsonWebKeySetAlgorithmFactory(string keySet, IJsonSerializerFactory jsonSerializerFactory)
{
_webKeysCollection = new JwtWebKeysCollection(keySet, jsonSerializerFactory);
}

public IJwtAlgorithm Create(JwtDecoderContext context)
{
if (string.IsNullOrEmpty(context.Header.KeyId))
throw new SignatureVerificationException("The key id is missing in the token header");

var key = _webKeysCollection.Find(context.Header.KeyId);

if (key == null)
throw new SignatureVerificationException("The key id is not presented in the JSON Web key set");

if (key.KeyType != "RSA")
throw new NotSupportedException($"JSON Web key type {key.KeyType} currently is not supported");

#if NETSTANDARD2_1 || NET6_0_OR_GREATER
var rsaParameters = new RSAParameters
{
Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(key.Modulus),
Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(key.Exponent)
};

var rsa = RSA.Create(rsaParameters);

var rsaAlgorithmFactory = new RSAlgorithmFactory(rsa);

return rsaAlgorithmFactory.Create(context);
#else
throw new NotImplementedException("Not implemented yet");
#endif
}
}
}
59 changes: 59 additions & 0 deletions src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;

namespace JWT.Jwk;

/// <summary>
/// Based on Microsoft.AspNetCore.WebUtilities.WebEncoders
/// </summary>
internal static class JwtWebKeyPropertyValuesEncoder
{
public static byte[] Base64UrlDecode(string input)
{
if (input == null)
return null;

var inputLength = input.Length;

var paddingCharsCount = GetNumBase64PaddingCharsToAddForDecode(inputLength);

var buffer = new char[inputLength + paddingCharsCount];

for (var i = 0; i < inputLength; ++i)
{
var symbol = input[i];

switch (symbol)
{
case '-':
buffer[i] = '+';
break;
case '_':
buffer[i] = '/';
break;
default:
buffer[i] = symbol;
break;
}
}

for (var i = input.Length; i < buffer.Length; ++i)
buffer[i] = '=';

return Convert.FromBase64CharArray(buffer, 0, buffer.Length);
}

private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
{
switch (inputLength % 4)
{
case 0:
return 0;
case 2:
return 2;
case 3:
return 1;
default:
throw new FormatException($"Malformed input: {inputLength} is an invalid input length.");
}
}
}
21 changes: 11 additions & 10 deletions tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
using JWT.Tests.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace JWT.Tests.Jwk;

[TestClass]
public class JwtWebKeysCollectionTests
namespace JWT.Tests.Jwk
{
[TestMethod]
public void Should_Find_Json_Web_Key_By_KeyId()
[TestClass]
public class JwtWebKeysCollectionTests
{
var serializerFactory = new DefaultJsonSerializerFactory();
[TestMethod]
public void Should_Find_Json_Web_Key_By_KeyId()
{
var serializerFactory = new DefaultJsonSerializerFactory();

var collection = new JwtWebKeysCollection(TestData.JsonWebKeySet, serializerFactory);
var collection = new JwtWebKeysCollection(TestData.JsonWebKeySet, serializerFactory);

var jwk = collection.Find(TestData.ServerRsaPublicThumbprint1);
var jwk = collection.Find(TestData.ServerRsaPublicThumbprint1);

Assert.IsNotNull(jwk);
Assert.IsNotNull(jwk);
}
}
}
27 changes: 26 additions & 1 deletion tests/JWT.Tests.Common/JwtDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using JWT.Algorithms;
using JWT.Builder;
using JWT.Exceptions;
using JWT.Jwk;
using JWT.Serializers;
using JWT.Tests.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -571,7 +572,31 @@ public void DecodeToObject_Should_Throw_Exception_On_Null_NotBefore_Claim()
.Throw<SignatureVerificationException>()
.WithMessage("Claim 'nbf' must be a number.", "because the invalid 'nbf' must result in an exception on decoding");
}


#if NETSTANDARD2_1 || NET6_0_OR_GREATER
[TestMethod]
public void Should_Decode_With_Json_Web_Keys()
{
var serializer = CreateSerializer();

var validator = new JwtValidator(serializer, new UtcDateTimeProvider());

var urlEncoder = new JwtBase64UrlEncoder();

var algorithmFactory = new JwtJsonWebKeySetAlgorithmFactory(TestData.JsonWebKeySet, serializer);

var decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithmFactory);

var customer = decoder.DecodeToObject<Customer>(TestData.TokenByAsymmetricAlgorithm);

Assert.IsNotNull(customer);

customer
.Should()
.BeEquivalentTo(TestData.Customer);
}
#endif

private static IJsonSerializer CreateSerializer() =>
new DefaultJsonSerializerFactory().Create();
}
Expand Down

0 comments on commit 96e1c76

Please sign in to comment.