From 96e1c76fc15968dd6a531b3e3c7d3b6e8d0fe511 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sat, 27 Jan 2024 13:28:48 +0200 Subject: [PATCH] add a new algorithm factory to create an algorithms based on JSON Web key sets --- src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs | 6 ++ .../Jwk/JwtJsonWebKeySetAlgorithmFactory.cs | 68 +++++++++++++++++++ src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs | 59 ++++++++++++++++ .../Jwk/JwtWebKeysCollectionTests.cs | 21 +++--- tests/JWT.Tests.Common/JwtDecoderTests.cs | 27 +++++++- 5 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs create mode 100644 src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs create mode 100644 src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs diff --git a/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs b/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs new file mode 100644 index 000000000..ffe2bb843 --- /dev/null +++ b/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs @@ -0,0 +1,6 @@ +namespace JWT.Jwk; + +public interface IJwtWebKeysCollectionFactory +{ + JwtWebKeysCollection CreateKeys(); +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs new file mode 100644 index 000000000..8ad2f4b04 --- /dev/null +++ b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs @@ -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 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 + } + } +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs b/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs new file mode 100644 index 000000000..47ea57053 --- /dev/null +++ b/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs @@ -0,0 +1,59 @@ +using System; + +namespace JWT.Jwk; + +/// +/// Based on Microsoft.AspNetCore.WebUtilities.WebEncoders +/// +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."); + } + } +} \ No newline at end of file diff --git a/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs b/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs index e5723005c..0a2b5b067 100644 --- a/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs +++ b/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs @@ -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); + } } } \ No newline at end of file diff --git a/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index bcd134e46..3edd311d6 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -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; @@ -571,7 +572,31 @@ public void DecodeToObject_Should_Throw_Exception_On_Null_NotBefore_Claim() .Throw() .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(TestData.TokenByAsymmetricAlgorithm); + + Assert.IsNotNull(customer); + + customer + .Should() + .BeEquivalentTo(TestData.Customer); + } +#endif + private static IJsonSerializer CreateSerializer() => new DefaultJsonSerializerFactory().Create(); }