From 9c40aa94098a81966840876ce5f92ea24042ae87 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sat, 27 Jan 2024 12:09:54 +0200 Subject: [PATCH 01/47] add JSON Web Key definitions --- src/JWT/Jwk/JwtWebKey.cs | 47 +++++++++++++++++++++++++++++++++++++ src/JWT/Jwk/JwtWebKeySet.cs | 31 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/JWT/Jwk/JwtWebKey.cs create mode 100644 src/JWT/Jwk/JwtWebKeySet.cs diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs new file mode 100644 index 000000000..9258d5e53 --- /dev/null +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; + +#if MODERN_DOTNET +using System.Text.Json.Serialization; +#endif + +namespace JWT.Jwk +{ + /// + /// A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key + /// specifed by RFC 7517, see https://datatracker.ietf.org/doc/html/rfc7517 + /// + public sealed class JwtWebKey + { +#if MODERN_DOTNET + [System.Text.Json.Serialization.JsonConstructor] + public JwtWebKey() + { + + } +#endif + + [JsonProperty("kty")] +#if MODERN_DOTNET + [JsonPropertyName("kty")] +#endif + public string KeyType { get; set; } + + [JsonProperty("kid")] +#if MODERN_DOTNET + [JsonPropertyName("kid")] +#endif + public string KeyId { get; set; } + + [JsonProperty("n")] +#if MODERN_DOTNET + [JsonPropertyName("n")] +#endif + public string Modulus { get; set; } + + [JsonProperty("e")] +#if MODERN_DOTNET + [JsonPropertyName("e")] +#endif + public string Exponent { get; set; } + } +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtWebKeySet.cs b/src/JWT/Jwk/JwtWebKeySet.cs new file mode 100644 index 000000000..36efcd928 --- /dev/null +++ b/src/JWT/Jwk/JwtWebKeySet.cs @@ -0,0 +1,31 @@ + +using System.Collections.Generic; +using Newtonsoft.Json; + +#if MODERN_DOTNET +using System.Text.Json.Serialization; +#endif + +namespace JWT.Jwk +{ + /// + /// A JWK Set JSON data structure that represents a set of JSON Web Keys + /// specifed by RFC 7517, see https://datatracker.ietf.org/doc/html/rfc7517 + /// + public sealed class JwtWebKeySet + { +#if MODERN_DOTNET + [System.Text.Json.Serialization.JsonConstructor] + public JwtWebKeySet() + { + + } +#endif + + [JsonProperty("keys")] +#if MODERN_DOTNET + [JsonPropertyName("keys")] +#endif + public IEnumerable Keys { get; set; } = null!; + } +} \ No newline at end of file From 79ea39aab99116964c89c8c480f2a20ebd0e3285 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sat, 27 Jan 2024 12:10:38 +0200 Subject: [PATCH 02/47] add Json Web keys collection with an ability to search keys by ids --- src/JWT/Jwk/IJwtWebKeysCollection.cs | 7 ++++ src/JWT/Jwk/JwtWebKeysCollection.cs | 38 +++++++++++++++++++ .../Jwk/JwtWebKeysCollectionTests.cs | 22 +++++++++++ tests/JWT.Tests.Common/Models/TestData.cs | 2 + 4 files changed, 69 insertions(+) create mode 100644 src/JWT/Jwk/IJwtWebKeysCollection.cs create mode 100644 src/JWT/Jwk/JwtWebKeysCollection.cs create mode 100644 tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs diff --git a/src/JWT/Jwk/IJwtWebKeysCollection.cs b/src/JWT/Jwk/IJwtWebKeysCollection.cs new file mode 100644 index 000000000..6c96b10f2 --- /dev/null +++ b/src/JWT/Jwk/IJwtWebKeysCollection.cs @@ -0,0 +1,7 @@ +namespace JWT.Jwk +{ + public interface IJwtWebKeysCollection + { + JwtWebKey Find(string keyId); + } +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtWebKeysCollection.cs b/src/JWT/Jwk/JwtWebKeysCollection.cs new file mode 100644 index 000000000..591dd1fcc --- /dev/null +++ b/src/JWT/Jwk/JwtWebKeysCollection.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using JWT.Serializers; + +namespace JWT.Jwk +{ + public class JwtWebKeysCollection : IJwtWebKeysCollection + { + private readonly Dictionary _keys; + + public JwtWebKeysCollection(IEnumerable keys) + { + _keys = keys.ToDictionary(x => x.KeyId); + } + + public JwtWebKeysCollection(JwtWebKeySet keySet) : this(keySet.Keys) + { + + } + + public JwtWebKeysCollection(string keySet, IJsonSerializer serializer) + : this(serializer.Deserialize(keySet)) + { + + } + + public JwtWebKeysCollection(string keySet, IJsonSerializerFactory jsonSerializerFactory) + : this(keySet, jsonSerializerFactory.Create()) + { + + } + + public JwtWebKey Find(string keyId) + { + return _keys.TryGetValue(keyId, out var key) ? key : null; + } + } +} \ No newline at end of file diff --git a/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs b/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs new file mode 100644 index 000000000..e5723005c --- /dev/null +++ b/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs @@ -0,0 +1,22 @@ +using JWT.Jwk; +using JWT.Serializers; +using JWT.Tests.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JWT.Tests.Jwk; + +[TestClass] +public class JwtWebKeysCollectionTests +{ + [TestMethod] + public void Should_Find_Json_Web_Key_By_KeyId() + { + var serializerFactory = new DefaultJsonSerializerFactory(); + + var collection = new JwtWebKeysCollection(TestData.JsonWebKeySet, serializerFactory); + + var jwk = collection.Find(TestData.ServerRsaPublicThumbprint1); + + Assert.IsNotNull(jwk); + } +} \ No newline at end of file diff --git a/tests/JWT.Tests.Common/Models/TestData.cs b/tests/JWT.Tests.Common/Models/TestData.cs index 3db70255c..5fe6cf8fb 100644 --- a/tests/JWT.Tests.Common/Models/TestData.cs +++ b/tests/JWT.Tests.Common/Models/TestData.cs @@ -67,6 +67,8 @@ public class TestDataSystemTextSerializerDecorated public const string TokenByAsymmetricAlgorithm = "eyJraWQiOiJDRkFFQUUyRDY1MEE2Q0E5ODYyNTc1REU1NDM3MUVBOTgwNjQzODQ5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoyMTQ3NDgzNjQ4LCJGaXJzdE5hbWUiOiJKZXN1cyIsIkFnZSI6MzN9.ZeGfWN3kBHZLiSh4jzzn6kx7F6lNu5OsowZW0Sv-_wpSgQO2_QXFUPLx23wm4J9rjMGQlSksEtCLd_X3iiBOBLbxAUWzdj59iJIAh485unZj12sBJ7KHDVsOMc6DcSJdwRo9S9yiJ_RJ57R-dn4uRdZTBXBZHrrmb35UjaAG6hFfu5d1Ap4ZjLxqDJGl0Wo4j5l6vR8HFpmiFHvqPQ4apjqkBGnitJ7oghbeRX0SIVNSkXbBDp3i9pC-hxzs2oHZC9ys0rJlfpxLls3MV4oQbQ7m6W9MrwwsdObJHI7PiTNfObLKdgySi6WkQS7rwXVz0DqRa8TXv8_USkvhsyGLMQ"; + public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"}]}"; + public static readonly IDictionary DictionaryPayload = new Dictionary { { nameof(Customer.FirstName), Customer.FirstName }, From 923f90c9be99bed37aeaadd3bf9b69eb25968a5a Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sat, 27 Jan 2024 13:28:48 +0200 Subject: [PATCH 03/47] 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(); } From 72613d2c1cea0b8d7618a41aa24b7300ff15cbd3 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 11:01:50 +0200 Subject: [PATCH 04/47] add JWKS support on .net framework --- src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs | 8 +++----- tests/JWT.Tests.Common/JwtDecoderTests.cs | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs index 8ad2f4b04..13b7406f4 100644 --- a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs @@ -48,21 +48,19 @@ public IJwtAlgorithm Create(JwtDecoderContext context) 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 rsa = RSA.Create(); + + rsa.ImportParameters(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/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index 3edd311d6..9291179b1 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -573,7 +573,6 @@ public void DecodeToObject_Should_Throw_Exception_On_Null_NotBefore_Claim() .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() { @@ -594,8 +593,7 @@ public void Should_Decode_With_Json_Web_Keys() customer .Should() .BeEquivalentTo(TestData.Customer); - } -#endif + } private static IJsonSerializer CreateSerializer() => new DefaultJsonSerializerFactory().Create(); From 0f1d3bb287cdc0b47b616fb3294718c930405842 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 11:10:51 +0200 Subject: [PATCH 05/47] add a test --- tests/JWT.Tests.Common/JwtDecoderTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index 9291179b1..e7d5d6f8d 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -595,6 +595,26 @@ public void Should_Decode_With_Json_Web_Keys() .BeEquivalentTo(TestData.Customer); } + [TestMethod] + public void DecodeToObject_With_Json_Web_keys_Should_Throw_Exception_If_Key_Is_Missing_In_Token() + { + 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); + + Action action = () => decoder.DecodeToObject(TestData.Token); + + action.Should() + .Throw() + .WithMessage("The key id is missing in the token header"); + } + private static IJsonSerializer CreateSerializer() => new DefaultJsonSerializerFactory().Create(); } From bcade658a468fe7425034c69c5d2f829c7bf2052 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 11:26:40 +0200 Subject: [PATCH 06/47] add a test --- tests/JWT.Tests.Common/JwtDecoderTests.cs | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index e7d5d6f8d..fc7ef89d2 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -615,6 +615,36 @@ public void DecodeToObject_With_Json_Web_keys_Should_Throw_Exception_If_Key_Is_M .WithMessage("The key id is missing in the token header"); } + [TestMethod] + public void DecodeToObject_With_Json_Web_keys_Should_Throw_Exception_If_Key_Is_Not_In_Collection() + { + 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); + + const string key = TestData.Secret; + + var token = JwtBuilder.Create() + .WithAlgorithm(TestData.HMACSHA256Algorithm) + .WithSecret(key) + .AddHeader(HeaderName.KeyId, "42") + .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) + .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) + .Encode(); + + Action action = () => decoder.DecodeToObject(token); + + action.Should() + .Throw() + .WithMessage("The key id is not presented in the JSON Web key set"); + } + private static IJsonSerializer CreateSerializer() => new DefaultJsonSerializerFactory().Create(); } From 9a76799f7fafc4ffc0da5d0e336a907c1515e64a Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 12:59:53 +0200 Subject: [PATCH 07/47] incapsulate creating algorithm from specific JWK --- .../InvalidJsonWebKeyTypeException.cs | 13 ++++ src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 64 +++++++++++++++++++ .../Jwk/JwtJsonWebKeySetAlgorithmFactory.cs | 18 +----- 3 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs create mode 100644 src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs diff --git a/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs b/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs new file mode 100644 index 000000000..542fcd72b --- /dev/null +++ b/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs @@ -0,0 +1,13 @@ +using System; + +namespace JWT.Exceptions +{ + public class InvalidJsonWebKeyTypeException : ArgumentOutOfRangeException + { + public InvalidJsonWebKeyTypeException(string keyType) + : base($"{keyType} is not defined in RFC7518") + { + + } + } +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs new file mode 100644 index 000000000..892e874e8 --- /dev/null +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -0,0 +1,64 @@ +using System; +using System.Security.Cryptography; +using JWT.Algorithms; +using JWT.Exceptions; + +namespace JWT.Jwk +{ + internal sealed class JwtJsonWebKeyAlgorithmFactory : IAlgorithmFactory + { + private readonly JwtWebKey _key; + + public JwtJsonWebKeyAlgorithmFactory(JwtWebKey key) + { + _key = key; + } + + public IJwtAlgorithm Create(JwtDecoderContext context) + { + switch (_key.KeyType) + { + case "RSA": + return CreateRSAAlgorithm(context); + + case "EC": + return CreateECDSAAlgorithm(context); + + case "oct": + return CreateHMACSHAAlgorithm(context); + + default: + throw new InvalidJsonWebKeyTypeException(_key.KeyType); + } + } + + private IJwtAlgorithm CreateRSAAlgorithm(JwtDecoderContext context) + { + var rsaParameters = new RSAParameters + { + Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Modulus), + Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Exponent) + }; + + var publicKey = RSA.Create(); + + publicKey.ImportParameters(rsaParameters); + + var algorithmFactory = new RSAlgorithmFactory(publicKey); + + return algorithmFactory.Create(context); + } + + private IJwtAlgorithm CreateECDSAAlgorithm(JwtDecoderContext context) + { + throw new NotImplementedException("TODO: implement me!"); + } + + private static IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) + { + var algorithmFactory = new HMACSHAAlgorithmFactory(); + + return algorithmFactory.Create(context); + } + } +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs index 13b7406f4..bf2984a03 100644 --- a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Security.Cryptography; using JWT.Algorithms; using JWT.Exceptions; using JWT.Serializers; @@ -45,22 +44,9 @@ public IJwtAlgorithm Create(JwtDecoderContext context) 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"); + var algorithmFactory = new JwtJsonWebKeyAlgorithmFactory(key); - var rsaParameters = new RSAParameters - { - Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(key.Modulus), - Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(key.Exponent) - }; - - var rsa = RSA.Create(); - - rsa.ImportParameters(rsaParameters); - - var rsaAlgorithmFactory = new RSAlgorithmFactory(rsa); - - return rsaAlgorithmFactory.Create(context); + return algorithmFactory.Create(context); } } } \ No newline at end of file From 3b6b54275600fdcf48864805a42d1882e9ac117c Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 13:06:41 +0200 Subject: [PATCH 08/47] add EC algorithm specific properties --- src/JWT/Jwk/JwtWebKey.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index 9258d5e53..20bb944a0 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -43,5 +43,23 @@ public JwtWebKey() [JsonPropertyName("e")] #endif public string Exponent { get; set; } + + [JsonProperty("crv")] +#if MODERN_DOTNET + [JsonPropertyName("crv")] +#endif + public string EllipticCurveType { get; set; } + + [JsonProperty("x")] +#if MODERN_DOTNET + [JsonPropertyName("x")] +#endif + public string EllipticCurveX { get; set; } + + [JsonProperty("y")] +#if MODERN_DOTNET + [JsonPropertyName("y")] +#endif + public string EllipticCurveY { get; set; } } } \ No newline at end of file From c64bd7b7dff3116452728c7f3183ff84d11fc1ac Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 13:27:23 +0200 Subject: [PATCH 09/47] implement elliptic curve algorithms creation --- ...lidJsonWebKeyEllipticCurveTypeException.cs | 13 ++++++ src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 42 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs diff --git a/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs b/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs new file mode 100644 index 000000000..fddb213bd --- /dev/null +++ b/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs @@ -0,0 +1,13 @@ +using System; + +namespace JWT.Exceptions +{ + public class InvalidJsonWebKeyEllipticCurveTypeException : ArgumentOutOfRangeException + { + public InvalidJsonWebKeyEllipticCurveTypeException(string ellipticCurveType) + : base($"{ellipticCurveType} is not defined in RFC751") + { + + } + } +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index 892e874e8..4ed18df1a 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -1,4 +1,3 @@ -using System; using System.Security.Cryptography; using JWT.Algorithms; using JWT.Exceptions; @@ -51,7 +50,26 @@ private IJwtAlgorithm CreateRSAAlgorithm(JwtDecoderContext context) private IJwtAlgorithm CreateECDSAAlgorithm(JwtDecoderContext context) { - throw new NotImplementedException("TODO: implement me!"); +#if NETSTANDARD2_0 || NET6_0_OR_GREATER + var parameters = new ECParameters + { + Curve = GetEllipticCurve(), + Q = new ECPoint + { + X = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurveX), + Y = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurveY) + } + }; + + var publicKey = ECDsa.Create(parameters); + + var algorithmFactory = new ECDSAAlgorithmFactory(publicKey); +#else + // will throw NotImplementedException on algorithmFactory.Create invocation. ECDSA algorithms are implemented for .NET Standard 2.0 or higher + var algorithmFactory = new ECDSAAlgorithmFactory(); +#endif + + return algorithmFactory.Create(context); } private static IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) @@ -60,5 +78,25 @@ private static IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) return algorithmFactory.Create(context); } + +#if NETSTANDARD2_0 || NET6_0_OR_GREATER + private ECCurve GetEllipticCurve() + { + switch (_key.EllipticCurveType) + { + case "P-256": + return ECCurve.NamedCurves.nistP256; + + case "P-384": + return ECCurve.NamedCurves.nistP384; + + case "P-521": + return ECCurve.NamedCurves.nistP521; + + default: + throw new InvalidJsonWebKeyEllipticCurveTypeException(_key.EllipticCurveType); + } + } +#endif } } \ No newline at end of file From 06be06cfe349196158b7e0f69f2eb65b2ba6378d Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 14:21:27 +0200 Subject: [PATCH 10/47] add a test --- tests/JWT.Tests.Common/JwtDecoderTests.cs | 39 ++++++++++++++++++++++- tests/JWT.Tests.Common/Models/TestData.cs | 15 ++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index fc7ef89d2..c40cb1f15 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; using AutoFixture; using FluentAssertions; using JWT.Algorithms; @@ -574,7 +575,7 @@ public void DecodeToObject_Should_Throw_Exception_On_Null_NotBefore_Claim() } [TestMethod] - public void Should_Decode_With_Json_Web_Keys() + public void Should_Decode_With_Json_Web_Keys_RSA() { var serializer = CreateSerializer(); @@ -595,6 +596,42 @@ public void Should_Decode_With_Json_Web_Keys() .BeEquivalentTo(TestData.Customer); } +#if NETSTANDARD2_0 || NET6_0_OR_GREATER + [TestMethod] + public void Should_Decode_With_Json_Web_Keys_EC() + { + const string key = TestData.Secret; + + var ecDsa = ECDsa.Create(TestData.EllipticCurvesParameters); + + var token = JwtBuilder.Create() + .WithAlgorithm(new ES256Algorithm(ecDsa, ecDsa)) + .WithSecret(key) + .AddHeader(HeaderName.KeyId, "EC-Test-Key") + .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) + .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) + .Encode(); + + 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(token); + + Assert.IsNotNull(customer); + + customer + .Should() + .BeEquivalentTo(TestData.Customer); + } +#endif + [TestMethod] public void DecodeToObject_With_Json_Web_keys_Should_Throw_Exception_If_Key_Is_Missing_In_Token() { diff --git a/tests/JWT.Tests.Common/Models/TestData.cs b/tests/JWT.Tests.Common/Models/TestData.cs index 5fe6cf8fb..56214740c 100644 --- a/tests/JWT.Tests.Common/Models/TestData.cs +++ b/tests/JWT.Tests.Common/Models/TestData.cs @@ -5,6 +5,7 @@ #if NETSTANDARD2_1 || NET6_0_OR_GREATER using System.Security.Cryptography; +using JWT.Jwk; #endif namespace JWT.Tests.Models @@ -67,7 +68,7 @@ public class TestDataSystemTextSerializerDecorated public const string TokenByAsymmetricAlgorithm = "eyJraWQiOiJDRkFFQUUyRDY1MEE2Q0E5ODYyNTc1REU1NDM3MUVBOTgwNjQzODQ5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoyMTQ3NDgzNjQ4LCJGaXJzdE5hbWUiOiJKZXN1cyIsIkFnZSI6MzN9.ZeGfWN3kBHZLiSh4jzzn6kx7F6lNu5OsowZW0Sv-_wpSgQO2_QXFUPLx23wm4J9rjMGQlSksEtCLd_X3iiBOBLbxAUWzdj59iJIAh485unZj12sBJ7KHDVsOMc6DcSJdwRo9S9yiJ_RJ57R-dn4uRdZTBXBZHrrmb35UjaAG6hFfu5d1Ap4ZjLxqDJGl0Wo4j5l6vR8HFpmiFHvqPQ4apjqkBGnitJ7oghbeRX0SIVNSkXbBDp3i9pC-hxzs2oHZC9ys0rJlfpxLls3MV4oQbQ7m6W9MrwwsdObJHI7PiTNfObLKdgySi6WkQS7rwXVz0DqRa8TXv8_USkvhsyGLMQ"; - public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"}]}"; + public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"},{\"kty\":\"EC\",\"kid\":\"EC-Test-Key\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0\",\"y\":\"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps\"}]}"; public static readonly IDictionary DictionaryPayload = new Dictionary { @@ -97,6 +98,18 @@ public class TestDataSystemTextSerializerDecorated #if NETSTANDARD2_1 || NET6_0_OR_GREATER public static readonly X509Certificate2 CertificateWithPrivateKey = CreateCertificate(); + // RFC7518. Appendix C sample + public static readonly ECParameters EllipticCurvesParameters = new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + Q = new ECPoint + { + X = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0"), + Y = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps") + }, + D = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo") + }; + private static X509Certificate2 CreateCertificate() { var rsa = RSA.Create(); From fc30b3f1dfa5300a5f4d7c9d96d6ffd3f6696909 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 15:00:02 +0200 Subject: [PATCH 11/47] add symmetric key property of the JWK --- src/JWT/Jwk/JwtWebKey.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index 20bb944a0..8956cf5e4 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -61,5 +61,11 @@ public JwtWebKey() [JsonPropertyName("y")] #endif public string EllipticCurveY { get; set; } + + [JsonProperty("k")] +#if MODERN_DOTNET + [JsonPropertyName("k")] +#endif + public string SymmetricKey { get; set; } } } \ No newline at end of file From 20e3d51af94a8b692295bc69483516e3b04403a2 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 17:34:33 +0200 Subject: [PATCH 12/47] add ISymmetircAlgorithm interface --- src/JWT/Algorithms/HMACSHAAlgorithm.cs | 2 +- src/JWT/Algorithms/ISymmetricAlgorithm.cs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/JWT/Algorithms/ISymmetricAlgorithm.cs diff --git a/src/JWT/Algorithms/HMACSHAAlgorithm.cs b/src/JWT/Algorithms/HMACSHAAlgorithm.cs index 6b53da268..5e4bb9981 100644 --- a/src/JWT/Algorithms/HMACSHAAlgorithm.cs +++ b/src/JWT/Algorithms/HMACSHAAlgorithm.cs @@ -2,7 +2,7 @@ namespace JWT.Algorithms { - public abstract class HMACSHAAlgorithm : IJwtAlgorithm + public abstract class HMACSHAAlgorithm : ISymmetricAlgorithm { /// public abstract string Name { get; } diff --git a/src/JWT/Algorithms/ISymmetricAlgorithm.cs b/src/JWT/Algorithms/ISymmetricAlgorithm.cs new file mode 100644 index 000000000..be408798d --- /dev/null +++ b/src/JWT/Algorithms/ISymmetricAlgorithm.cs @@ -0,0 +1,10 @@ +namespace JWT.Algorithms +{ + /// + /// Represents a symmetric algorithm to generate or validate JWT signature. + /// + public interface ISymmetricAlgorithm : IJwtAlgorithm + { + + } +} \ No newline at end of file From 528bc73214f57f5803a266af1062cb5c3d94614b Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 17:35:27 +0200 Subject: [PATCH 13/47] move symmetric algorithm validation to jwt validator --- src/JWT/IJwtValidator.cs | 13 ++++++++++++ src/JWT/JwtDecoder.cs | 43 ++-------------------------------------- src/JWT/JwtValidator.cs | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/JWT/IJwtValidator.cs b/src/JWT/IJwtValidator.cs index 464282f9e..4f8dd8359 100644 --- a/src/JWT/IJwtValidator.cs +++ b/src/JWT/IJwtValidator.cs @@ -28,6 +28,19 @@ public interface IJwtValidator /// The signature to validate with void Validate(string decodedPayload, IAsymmetricAlgorithm alg, byte[] bytesToSign, byte[] decodedSignature); + /// + /// Given the JWT, verifies its signature correctness. + /// + /// + /// Used by the symmetric algorithms only. + /// + /// The keys provided which one of them was used to sign the JWT + /// + /// + /// + /// + void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm alg, byte[] bytesToSign, byte[] decodedSignature); + /// /// Given the JWT, verifies its signature correctness without throwing an exception but returning it instead. /// diff --git a/src/JWT/JwtDecoder.cs b/src/JWT/JwtDecoder.cs index 7c19741de..1c8e1616b 100644 --- a/src/JWT/JwtDecoder.cs +++ b/src/JWT/JwtDecoder.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using JWT.Algorithms; using JWT.Builder; using JWT.Exceptions; @@ -249,9 +248,9 @@ public void Validate(JwtParts jwt, params byte[][] keys) { _jwtValidator.Validate(decodedPayload, asymmAlg, bytesToSign, decodedSignature); } - else + else if (algorithm is ISymmetricAlgorithm symmAlg) { - ValidSymmetricAlgorithm(keys, decodedPayload, algorithm, bytesToSign, decodedSignature); + _jwtValidator.Validate(keys, decodedPayload, symmAlg, bytesToSign, decodedSignature); } } @@ -264,44 +263,6 @@ private string Decode(JwtParts jwt) return GetString(decoded); } - private void ValidSymmetricAlgorithm(byte[][] keys, string decodedPayload, IJwtAlgorithm algorithm, byte[] bytesToSign, byte[] decodedSignature) - { - if (keys is null) - throw new ArgumentNullException(nameof(keys)); - if (!AllKeysHaveValues(keys)) - throw new ArgumentOutOfRangeException(nameof(keys)); - - // the signature on the token, with the leading = - var rawSignature = Convert.ToBase64String(decodedSignature); - - // the signatures re-created by the algorithm, with the leading = - var recreatedSignatures = keys.Select(key => Convert.ToBase64String(algorithm.Sign(key, bytesToSign))).ToArray(); - - _jwtValidator.Validate(decodedPayload, rawSignature, recreatedSignatures); - } - - private static bool AllKeysHaveValues(byte[][] keys) - { - if (keys is null) - return false; - - if (keys.Length == 0) - return false; - - return Array.TrueForAll(keys, key => KeyHasValue(key)); - } - - private static bool KeyHasValue(byte[] key) - { - if (key is null) - return false; - - if (key.Length == 0) - return false; - - return true; - } - private void ValidateNoneAlgorithm(JwtParts jwt) { var header = DecodeHeader(jwt); diff --git a/src/JWT/JwtValidator.cs b/src/JWT/JwtValidator.cs index e6d846fe2..fd41815a0 100644 --- a/src/JWT/JwtValidator.cs +++ b/src/JWT/JwtValidator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using JWT.Algorithms; using JWT.Exceptions; @@ -87,6 +88,22 @@ public void Validate(string decodedPayload, IAsymmetricAlgorithm alg, byte[] byt throw ex; } + public void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm alg, byte[] bytesToSign, byte[] decodedSignature) + { + if (keys is null) + throw new ArgumentNullException(nameof(keys)); + if (!AllKeysHaveValues(keys)) + throw new ArgumentOutOfRangeException(nameof(keys)); + + // the signature on the token, with the leading = + var rawSignature = Convert.ToBase64String(decodedSignature); + + // the signatures re-created by the algorithm, with the leading = + var recreatedSignatures = keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray(); + + Validate(decodedPayload, rawSignature, recreatedSignatures); + } + /// /// public bool TryValidate(string payloadJson, string signature, string decodedSignature, out Exception ex) @@ -264,5 +281,29 @@ private Exception ValidateNbfClaim(IReadOnlyPayloadDictionary payloadData, doubl return null; } + + + + private static bool AllKeysHaveValues(byte[][] keys) + { + if (keys is null) + return false; + + if (keys.Length == 0) + return false; + + return Array.TrueForAll(keys, key => KeyHasValue(key)); + } + + private static bool KeyHasValue(byte[] key) + { + if (key is null) + return false; + + if (key.Length == 0) + return false; + + return true; + } } } From 31ea350961715843a57b9ef5ec4a30cb1ab3b6a2 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 17:42:32 +0200 Subject: [PATCH 14/47] hmacs sha algorithms could have built-in key --- src/JWT/Algorithms/HMACSHA256Algorithm.cs | 10 ++++++++++ src/JWT/Algorithms/HMACSHA384Algorithm.cs | 10 ++++++++++ src/JWT/Algorithms/HMACSHA512Algorithm.cs | 12 +++++++++++- src/JWT/Algorithms/HMACSHAAlgorithm.cs | 14 +++++++++++++- src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs | 18 +++++++++++++++--- 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/JWT/Algorithms/HMACSHA256Algorithm.cs b/src/JWT/Algorithms/HMACSHA256Algorithm.cs index 072058d8b..3ac7cb303 100644 --- a/src/JWT/Algorithms/HMACSHA256Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA256Algorithm.cs @@ -7,6 +7,16 @@ namespace JWT.Algorithms /// public sealed class HMACSHA256Algorithm : HMACSHAAlgorithm { + public HMACSHA256Algorithm() + { + + } + + internal HMACSHA256Algorithm(byte[] key) : base(key) + { + + } + /// public override string Name => nameof(JwtAlgorithmName.HS256); diff --git a/src/JWT/Algorithms/HMACSHA384Algorithm.cs b/src/JWT/Algorithms/HMACSHA384Algorithm.cs index cac171668..b648245f3 100644 --- a/src/JWT/Algorithms/HMACSHA384Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA384Algorithm.cs @@ -7,6 +7,16 @@ namespace JWT.Algorithms /// public sealed class HMACSHA384Algorithm : HMACSHAAlgorithm { + public HMACSHA384Algorithm() + { + + } + + internal HMACSHA384Algorithm(byte[] key) : base(key) + { + + } + /// public override string Name => nameof(JwtAlgorithmName.HS384); diff --git a/src/JWT/Algorithms/HMACSHA512Algorithm.cs b/src/JWT/Algorithms/HMACSHA512Algorithm.cs index b5c043b47..a964d159a 100644 --- a/src/JWT/Algorithms/HMACSHA512Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA512Algorithm.cs @@ -6,7 +6,17 @@ namespace JWT.Algorithms /// HMAC using SHA-512 /// public sealed class HMACSHA512Algorithm : HMACSHAAlgorithm - { + { + public HMACSHA512Algorithm() + { + + } + + internal HMACSHA512Algorithm(byte[] key) : base(key) + { + + } + /// public override string Name => nameof(JwtAlgorithmName.HS512); diff --git a/src/JWT/Algorithms/HMACSHAAlgorithm.cs b/src/JWT/Algorithms/HMACSHAAlgorithm.cs index 5e4bb9981..4a241c3fa 100644 --- a/src/JWT/Algorithms/HMACSHAAlgorithm.cs +++ b/src/JWT/Algorithms/HMACSHAAlgorithm.cs @@ -4,6 +4,16 @@ namespace JWT.Algorithms { public abstract class HMACSHAAlgorithm : ISymmetricAlgorithm { + protected HMACSHAAlgorithm() + { + + } + + protected HMACSHAAlgorithm(byte[] key) + { + this.Key = key; + } + /// public abstract string Name { get; } @@ -13,10 +23,12 @@ public abstract class HMACSHAAlgorithm : ISymmetricAlgorithm /// public byte[] Sign(byte[] key, byte[] bytesToSign) { - using var sha = CreateAlgorithm(key); + using var sha = CreateAlgorithm(key ?? this.Key); return sha.ComputeHash(bytesToSign); } + public byte[] Key { get; } + protected abstract HMAC CreateAlgorithm(byte[] key); } } \ No newline at end of file diff --git a/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs b/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs index 918250749..2d66e900f 100644 --- a/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs +++ b/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs @@ -5,16 +5,28 @@ namespace JWT.Algorithms /// public class HMACSHAAlgorithmFactory : JwtAlgorithmFactory { + private readonly byte[] _key; + + public HMACSHAAlgorithmFactory() + { + + } + + public HMACSHAAlgorithmFactory(byte[] key) + { + _key = key; + } + protected override IJwtAlgorithm Create(JwtAlgorithmName algorithm) { switch (algorithm) { case JwtAlgorithmName.HS256: - return new HMACSHA256Algorithm(); + return new HMACSHA256Algorithm(_key); case JwtAlgorithmName.HS384: - return new HMACSHA384Algorithm(); + return new HMACSHA384Algorithm(_key); case JwtAlgorithmName.HS512: - return new HMACSHA512Algorithm(); + return new HMACSHA512Algorithm(_key); case JwtAlgorithmName.RS256: case JwtAlgorithmName.RS384: case JwtAlgorithmName.RS512: From f74489f880cfa16dfa3af42dab4a027cd90b4a76 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 17:43:16 +0200 Subject: [PATCH 15/47] algorithm factory reads the key from Json Web Key --- src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index 4ed18df1a..e9277bef0 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -72,9 +72,11 @@ private IJwtAlgorithm CreateECDSAAlgorithm(JwtDecoderContext context) return algorithmFactory.Create(context); } - private static IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) + private IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) { - var algorithmFactory = new HMACSHAAlgorithmFactory(); + var key = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SymmetricKey); + + var algorithmFactory = new HMACSHAAlgorithmFactory(key); return algorithmFactory.Create(context); } From e8d36dbb246cb9511c1c28d0044e1266869d3a5c Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 17:43:43 +0200 Subject: [PATCH 16/47] add a property to symetrical algorithm interface --- src/JWT/Algorithms/ISymmetricAlgorithm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT/Algorithms/ISymmetricAlgorithm.cs b/src/JWT/Algorithms/ISymmetricAlgorithm.cs index be408798d..23bd737a8 100644 --- a/src/JWT/Algorithms/ISymmetricAlgorithm.cs +++ b/src/JWT/Algorithms/ISymmetricAlgorithm.cs @@ -5,6 +5,6 @@ namespace JWT.Algorithms /// public interface ISymmetricAlgorithm : IJwtAlgorithm { - + byte[] Key { get; } } } \ No newline at end of file From 173a5aae40efc9ac3372adfbecce1deb7718be33 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 17:55:22 +0200 Subject: [PATCH 17/47] use symmetric key from the JWKS --- src/JWT/JwtValidator.cs | 2 +- tests/JWT.Tests.Common/JwtDecoderTests.cs | 34 ++++++++++++++++++++++- tests/JWT.Tests.Common/Models/TestData.cs | 2 +- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/JWT/JwtValidator.cs b/src/JWT/JwtValidator.cs index fd41815a0..e09744892 100644 --- a/src/JWT/JwtValidator.cs +++ b/src/JWT/JwtValidator.cs @@ -92,7 +92,7 @@ public void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm a { if (keys is null) throw new ArgumentNullException(nameof(keys)); - if (!AllKeysHaveValues(keys)) + if (alg.Key == null && !AllKeysHaveValues(keys)) throw new ArgumentOutOfRangeException(nameof(keys)); // the signature on the token, with the leading = diff --git a/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index c40cb1f15..0d397cca6 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -629,9 +629,41 @@ public void Should_Decode_With_Json_Web_Keys_EC() customer .Should() .BeEquivalentTo(TestData.Customer); - } + } #endif + [TestMethod] + public void Should_Decode_With_Json_Web_Keys_() + { + const string key = TestData.Secret; + + var token = JwtBuilder.Create() + .WithAlgorithm(new HMACSHA256Algorithm()) + .WithSecret(key) + .AddHeader(HeaderName.KeyId, "OCT-Test-Key") + .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) + .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) + .Encode(); + + 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(token); + + Assert.IsNotNull(customer); + + customer + .Should() + .BeEquivalentTo(TestData.Customer); + } + [TestMethod] public void DecodeToObject_With_Json_Web_keys_Should_Throw_Exception_If_Key_Is_Missing_In_Token() { diff --git a/tests/JWT.Tests.Common/Models/TestData.cs b/tests/JWT.Tests.Common/Models/TestData.cs index 56214740c..1d6a3e1fb 100644 --- a/tests/JWT.Tests.Common/Models/TestData.cs +++ b/tests/JWT.Tests.Common/Models/TestData.cs @@ -68,7 +68,7 @@ public class TestDataSystemTextSerializerDecorated public const string TokenByAsymmetricAlgorithm = "eyJraWQiOiJDRkFFQUUyRDY1MEE2Q0E5ODYyNTc1REU1NDM3MUVBOTgwNjQzODQ5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoyMTQ3NDgzNjQ4LCJGaXJzdE5hbWUiOiJKZXN1cyIsIkFnZSI6MzN9.ZeGfWN3kBHZLiSh4jzzn6kx7F6lNu5OsowZW0Sv-_wpSgQO2_QXFUPLx23wm4J9rjMGQlSksEtCLd_X3iiBOBLbxAUWzdj59iJIAh485unZj12sBJ7KHDVsOMc6DcSJdwRo9S9yiJ_RJ57R-dn4uRdZTBXBZHrrmb35UjaAG6hFfu5d1Ap4ZjLxqDJGl0Wo4j5l6vR8HFpmiFHvqPQ4apjqkBGnitJ7oghbeRX0SIVNSkXbBDp3i9pC-hxzs2oHZC9ys0rJlfpxLls3MV4oQbQ7m6W9MrwwsdObJHI7PiTNfObLKdgySi6WkQS7rwXVz0DqRa8TXv8_USkvhsyGLMQ"; - public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"},{\"kty\":\"EC\",\"kid\":\"EC-Test-Key\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0\",\"y\":\"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps\"}]}"; + public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"},{\"kty\":\"EC\",\"kid\":\"EC-Test-Key\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0\",\"y\":\"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps\"},{\"kty\":\"oct\",\"kid\":\"OCT-Test-Key\",\"use\":\"sig\",\"k\":\"R1FEc3RjS3N4ME5IalBPdVhPWWc1TWJlSjFYVDB1Rml3RFZ2VkJyaw\"}]}"; public static readonly IDictionary DictionaryPayload = new Dictionary { From 28afdb6e4fcf6b0e2742880bf5fbe53d4f8c4dc1 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 18:58:03 +0200 Subject: [PATCH 18/47] add new Jwt Builder methods to make a decoder with JWKS --- src/JWT/Builder/JwtBuilder.cs | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/JWT/Builder/JwtBuilder.cs b/src/JWT/Builder/JwtBuilder.cs index cf5570cd2..5054be602 100644 --- a/src/JWT/Builder/JwtBuilder.cs +++ b/src/JWT/Builder/JwtBuilder.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using JWT.Algorithms; +using JWT.Jwk; using JWT.Serializers; using Newtonsoft.Json; @@ -170,6 +171,50 @@ public JwtBuilder WithAlgorithmFactory(IAlgorithmFactory algFactory) return this; } + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(JwtWebKeysCollection webKeysCollection) + { + _algFactory = new JwtJsonWebKeySetAlgorithmFactory(webKeysCollection); + return this; + } + + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(Func getJsonWebKeys) + { + _algFactory = new JwtJsonWebKeySetAlgorithmFactory(getJsonWebKeys); + return this; + } + + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(IJwtWebKeysCollectionFactory webKeysCollectionFactory) + { + _algFactory = new JwtJsonWebKeySetAlgorithmFactory(webKeysCollectionFactory); + return this; + } + + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(string keySet) + { + _algFactory = new JwtJsonWebKeySetAlgorithmFactory(keySet, _jsonSerializerFactory); + return this; + } + /// /// Sets JWT algorithm. /// From be352ca416544c429d75b6f5b5bcca16ca20b74c Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 19:16:20 +0200 Subject: [PATCH 19/47] cleanup the test --- tests/JWT.Tests.Common/JwtDecoderTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index 0d397cca6..c39457d52 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -600,13 +600,10 @@ public void Should_Decode_With_Json_Web_Keys_RSA() [TestMethod] public void Should_Decode_With_Json_Web_Keys_EC() { - const string key = TestData.Secret; - var ecDsa = ECDsa.Create(TestData.EllipticCurvesParameters); var token = JwtBuilder.Create() .WithAlgorithm(new ES256Algorithm(ecDsa, ecDsa)) - .WithSecret(key) .AddHeader(HeaderName.KeyId, "EC-Test-Key") .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) From 4ad94e9bc11ddf7daf57cc1c7d557c9ebd3ab953 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 19:43:16 +0200 Subject: [PATCH 20/47] extend Jwt Builder with an ability to set JSON Web key --- src/JWT/Builder/JwtBuilder.cs | 60 +++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/JWT/Builder/JwtBuilder.cs b/src/JWT/Builder/JwtBuilder.cs index 5054be602..1ab9b8f22 100644 --- a/src/JWT/Builder/JwtBuilder.cs +++ b/src/JWT/Builder/JwtBuilder.cs @@ -35,6 +35,8 @@ public sealed class JwtBuilder private IAlgorithmFactory _algFactory; private byte[][] _secrets; + private JwtWebKeysCollection _webKeysCollection; + /// /// Creates a new instance of instance /// @@ -178,6 +180,7 @@ public JwtBuilder WithAlgorithmFactory(IAlgorithmFactory algFactory) /// Current builder instance. public JwtBuilder WithJsonWebKeySet(JwtWebKeysCollection webKeysCollection) { + _webKeysCollection = webKeysCollection; _algFactory = new JwtJsonWebKeySetAlgorithmFactory(webKeysCollection); return this; } @@ -189,8 +192,7 @@ public JwtBuilder WithJsonWebKeySet(JwtWebKeysCollection webKeysCollection) /// Current builder instance. public JwtBuilder WithJsonWebKeySet(Func getJsonWebKeys) { - _algFactory = new JwtJsonWebKeySetAlgorithmFactory(getJsonWebKeys); - return this; + return WithJsonWebKeySet(getJsonWebKeys()); } /// @@ -200,8 +202,7 @@ public JwtBuilder WithJsonWebKeySet(Func getJsonWebKeys) /// Current builder instance. public JwtBuilder WithJsonWebKeySet(IJwtWebKeysCollectionFactory webKeysCollectionFactory) { - _algFactory = new JwtJsonWebKeySetAlgorithmFactory(webKeysCollectionFactory); - return this; + return WithJsonWebKeySet(webKeysCollectionFactory.CreateKeys()); } /// @@ -211,8 +212,55 @@ public JwtBuilder WithJsonWebKeySet(IJwtWebKeysCollectionFactory webKeysCollecti /// Current builder instance. public JwtBuilder WithJsonWebKeySet(string keySet) { - _algFactory = new JwtJsonWebKeySetAlgorithmFactory(keySet, _jsonSerializerFactory); - return this; + return WithJsonWebKeySet(new JwtWebKeysCollection(keySet, _jsonSerializerFactory)); + } + + /// + /// Sets Json Web Key + /// + /// Key id in the JSON Web Key Set + /// JWT algorithm name + /// Current builder instance + /// + public JwtBuilder WithJsonWebKey(string keyId, JwtAlgorithmName algorithmName) + { + if (_webKeysCollection == null) + throw new InvalidOperationException("JSON Web Key Set collection has not been initialized yet"); + + var key = _webKeysCollection.Find(keyId); + + if (key == null) + throw new InvalidOperationException("The key id is not presented in the JSON Web key set"); + + return WithJsonWebKey(key, algorithmName); + } + + /// + /// Sets Json Web Key + /// + /// JSON Web Key + /// JWT algorithm name + /// Current builder instance + /// + public JwtBuilder WithJsonWebKey(JwtWebKey key, JwtAlgorithmName algorithmName) + { + if (key == null) + throw new ArgumentNullException(nameof(key), "JSON Web Key has not been provided"); + + var factory = new JwtJsonWebKeyAlgorithmFactory(key); + + var context = new JwtDecoderContext + { + Header = new JwtHeader + { + Algorithm = algorithmName.ToString(), + KeyId = key.KeyId + } + }; + + _algorithm = factory.Create(context); + + return AddHeader(HeaderName.KeyId, key.KeyId); } /// From 61cfe5055674efd09902b85318bc4ae0ee2ac169 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 20:00:01 +0200 Subject: [PATCH 21/47] JWT can encode with symmetrical key from the JWKS --- src/JWT/JwtEncoder.cs | 3 ++- .../Builder/JwtBuilderEncodeTests.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/JWT/JwtEncoder.cs b/src/JWT/JwtEncoder.cs index 126e9f7c1..940b5f58d 100644 --- a/src/JWT/JwtEncoder.cs +++ b/src/JWT/JwtEncoder.cs @@ -49,7 +49,8 @@ public string Encode(IDictionary extraHeaders, object payload, b var algorithm = _algFactory.Create(null); if (algorithm is null) throw new ArgumentNullException(nameof(algorithm)); - if (!algorithm.IsAsymmetric() && key is null && algorithm is not NoneAlgorithm) + + if (algorithm is ISymmetricAlgorithm symmetricAlgorithm && key is null && symmetricAlgorithm.Key is null) throw new ArgumentNullException(nameof(key)); var header = extraHeaders is null ? diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index 9a6d16992..d8029a8bb 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs @@ -414,6 +414,19 @@ public void Encode_Test_Bug438() } #endif + [TestMethod] + public void Encode_With_Symmetrical_WebKey_From_WebKey_Set_Should_Return_Token() + { + var token = JwtBuilder.Create() + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .WithJsonWebKey("OCT-Test-Key", JwtAlgorithmName.HS256) + .AddClaim("Data", TestData.Customer) + .Encode(); + + token.Should() + .Be("eyJraWQiOiJPQ1QtVGVzdC1LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJEYXRhIjp7IkZpcnN0TmFtZSI6Ikplc3VzIiwiQWdlIjozM319.GFNIchXNoTLYvKT2mvO_s1_MBW-aBSfkQqxHWp7L-wo"); + } + private sealed class CustomFactory : IAlgorithmFactory { public IJwtAlgorithm Create(JwtDecoderContext context) => From 25ce0e134a8d3450cf84ab1c8f770e22e39e097e Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 20:23:34 +0200 Subject: [PATCH 22/47] add JwtWebKey properies summaries --- src/JWT/Jwk/JwtWebKey.cs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index 8956cf5e4..96a0ec392 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -20,48 +20,79 @@ public JwtWebKey() } #endif + /// + /// The "kty" parameter which defines key type which is defined by RFC7518 specification. + /// Valid values are "EC" (Elliptic Curve), "RSA" and "oct" (octet sequence used to represent symmetric keys) + /// [JsonProperty("kty")] #if MODERN_DOTNET [JsonPropertyName("kty")] #endif public string KeyType { get; set; } + /// + /// The "kid" parameter which defines key id + /// [JsonProperty("kid")] #if MODERN_DOTNET [JsonPropertyName("kid")] #endif public string KeyId { get; set; } + /// + /// The "n" (modulus) parameter contains the modulus value for the RSA public key. It is represented as a Base64urlUInt-encoded value. + /// ("kty") must be "RSA" + /// [JsonProperty("n")] #if MODERN_DOTNET [JsonPropertyName("n")] #endif public string Modulus { get; set; } + /// + /// The "e" (exponent) parameter contains the exponent value for the RSA public key. It is represented as a Base64urlUInt-encoded value. + /// ("kty") must be "RSA" + /// [JsonProperty("e")] #if MODERN_DOTNET [JsonPropertyName("e")] #endif public string Exponent { get; set; } + /// + /// The "crv" (curve) parameter identifies the cryptographic curve used with the key. RFC7518 defines the following valid values: + /// "P-256", "P-384", "P-521" ("kty") must be "EC" + /// [JsonProperty("crv")] #if MODERN_DOTNET [JsonPropertyName("crv")] #endif public string EllipticCurveType { get; set; } + /// + /// The "x" (x coordinate) parameter contains the x coordinate for the Elliptic Curve point. It is represented as the base64url encoding of + /// the octet string representation of the coordinate. ("kty") must be "EC" + /// [JsonProperty("x")] #if MODERN_DOTNET [JsonPropertyName("x")] #endif public string EllipticCurveX { get; set; } + /// + /// The "y" (y coordinate) parameter contains the y coordinate for the Elliptic Curve point. It is represented as the base64url encoding of + /// the octet string representation of the coordinate. ("kty") must be "EC" + /// [JsonProperty("y")] #if MODERN_DOTNET [JsonPropertyName("y")] #endif public string EllipticCurveY { get; set; } + /// + /// The "k" (key value) parameter contains the value of the symmetric (or other single-valued) key. It is represented as the base64url + /// encoding of the octet sequence containing the key value. ("kty") must be "oct" + /// [JsonProperty("k")] #if MODERN_DOTNET [JsonPropertyName("k")] From ab3b341fe4a56309c3cf254142eba8cf51141fd0 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 20:27:13 +0200 Subject: [PATCH 23/47] add elliptic curve private key property of the JWK --- src/JWT/Jwk/JwtWebKey.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index 96a0ec392..f7231185d 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -89,6 +89,15 @@ public JwtWebKey() #endif public string EllipticCurveY { get; set; } + /// + /// The "d" parameter. Represents a private key for the Elliptic Curve algorithm. ("kty") must be "EC" + /// + [JsonProperty("d")] +#if MODERN_DOTNET + [JsonPropertyName("d")] +#endif + public string EllipticCurvePrivateKey { get; set; } + /// /// The "k" (key value) parameter contains the value of the symmetric (or other single-valued) key. It is represented as the base64url /// encoding of the octet sequence containing the key value. ("kty") must be "oct" From 67a88fa8d1268b1380510744b1014dfdec534e51 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 20:47:17 +0200 Subject: [PATCH 24/47] allow to encode with a key from Json Web Key set with elliptic curve algorithms --- src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 9 +++++--- .../Builder/JwtBuilderEncodeTests.cs | 22 +++++++++++++++++-- tests/JWT.Tests.Common/Models/TestData.cs | 2 +- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index e9277bef0..290b86dba 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -58,12 +58,15 @@ private IJwtAlgorithm CreateECDSAAlgorithm(JwtDecoderContext context) { X = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurveX), Y = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurveY) - } + }, + D = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurvePrivateKey) }; - var publicKey = ECDsa.Create(parameters); + var key = ECDsa.Create(parameters); - var algorithmFactory = new ECDSAAlgorithmFactory(publicKey); + var algorithmFactory = parameters.D == null + ? new ECDSAAlgorithmFactory(key) + : new ECDSAAlgorithmFactory(key, key); #else // will throw NotImplementedException on algorithmFactory.Create invocation. ECDSA algorithms are implemented for .NET Standard 2.0 or higher var algorithmFactory = new ECDSAAlgorithmFactory(); diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index d8029a8bb..a82ca8c3a 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs @@ -2,10 +2,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Reflection; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; using AutoFixture; using FluentAssertions; using JWT.Algorithms; @@ -427,6 +425,26 @@ public void Encode_With_Symmetrical_WebKey_From_WebKey_Set_Should_Return_Token() .Be("eyJraWQiOiJPQ1QtVGVzdC1LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJEYXRhIjp7IkZpcnN0TmFtZSI6Ikplc3VzIiwiQWdlIjozM319.GFNIchXNoTLYvKT2mvO_s1_MBW-aBSfkQqxHWp7L-wo"); } +#if NETSTANDARD2_0 || NET6_0_OR_GREATER + [TestMethod] + public void Encode_With_Elliptic_Curve_WebKey_From_WebKey_Set_Should_Return_Token() + { + var token = JwtBuilder.Create() + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .WithJsonWebKey("EC-Test-Key", JwtAlgorithmName.ES256) + .Encode(TestData.Customer); + + token.Should().NotBeNullOrEmpty(); + + var decoded = JwtBuilder.Create() + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .Decode(token); + + decoded.Should() + .BeEquivalentTo(TestData.Customer); + } +#endif + private sealed class CustomFactory : IAlgorithmFactory { public IJwtAlgorithm Create(JwtDecoderContext context) => diff --git a/tests/JWT.Tests.Common/Models/TestData.cs b/tests/JWT.Tests.Common/Models/TestData.cs index 1d6a3e1fb..a8a1be224 100644 --- a/tests/JWT.Tests.Common/Models/TestData.cs +++ b/tests/JWT.Tests.Common/Models/TestData.cs @@ -68,7 +68,7 @@ public class TestDataSystemTextSerializerDecorated public const string TokenByAsymmetricAlgorithm = "eyJraWQiOiJDRkFFQUUyRDY1MEE2Q0E5ODYyNTc1REU1NDM3MUVBOTgwNjQzODQ5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoyMTQ3NDgzNjQ4LCJGaXJzdE5hbWUiOiJKZXN1cyIsIkFnZSI6MzN9.ZeGfWN3kBHZLiSh4jzzn6kx7F6lNu5OsowZW0Sv-_wpSgQO2_QXFUPLx23wm4J9rjMGQlSksEtCLd_X3iiBOBLbxAUWzdj59iJIAh485unZj12sBJ7KHDVsOMc6DcSJdwRo9S9yiJ_RJ57R-dn4uRdZTBXBZHrrmb35UjaAG6hFfu5d1Ap4ZjLxqDJGl0Wo4j5l6vR8HFpmiFHvqPQ4apjqkBGnitJ7oghbeRX0SIVNSkXbBDp3i9pC-hxzs2oHZC9ys0rJlfpxLls3MV4oQbQ7m6W9MrwwsdObJHI7PiTNfObLKdgySi6WkQS7rwXVz0DqRa8TXv8_USkvhsyGLMQ"; - public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"},{\"kty\":\"EC\",\"kid\":\"EC-Test-Key\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0\",\"y\":\"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps\"},{\"kty\":\"oct\",\"kid\":\"OCT-Test-Key\",\"use\":\"sig\",\"k\":\"R1FEc3RjS3N4ME5IalBPdVhPWWc1TWJlSjFYVDB1Rml3RFZ2VkJyaw\"}]}"; + public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"},{\"kty\":\"EC\",\"kid\":\"EC-Test-Key\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0\",\"y\":\"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps\",\"d\":\"0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo\"},{\"kty\":\"oct\",\"kid\":\"OCT-Test-Key\",\"use\":\"sig\",\"k\":\"R1FEc3RjS3N4ME5IalBPdVhPWWc1TWJlSjFYVDB1Rml3RFZ2VkJyaw\"}]}"; public static readonly IDictionary DictionaryPayload = new Dictionary { From de808ee75822c5bf4964246f8f94cf4272ed0004 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 20:55:50 +0200 Subject: [PATCH 25/47] adjust the test and found a bug --- src/JWT/JwtValidator.cs | 6 ++++-- .../Builder/JwtBuilderEncodeTests.cs | 13 +++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/JWT/JwtValidator.cs b/src/JWT/JwtValidator.cs index e09744892..1f417abfa 100644 --- a/src/JWT/JwtValidator.cs +++ b/src/JWT/JwtValidator.cs @@ -90,7 +90,7 @@ public void Validate(string decodedPayload, IAsymmetricAlgorithm alg, byte[] byt public void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm alg, byte[] bytesToSign, byte[] decodedSignature) { - if (keys is null) + if (alg.Key == null && keys is null) throw new ArgumentNullException(nameof(keys)); if (alg.Key == null && !AllKeysHaveValues(keys)) throw new ArgumentOutOfRangeException(nameof(keys)); @@ -99,7 +99,9 @@ public void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm a var rawSignature = Convert.ToBase64String(decodedSignature); // the signatures re-created by the algorithm, with the leading = - var recreatedSignatures = keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray(); + var recreatedSignatures = keys != null + ? keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray() + : new string[] { Convert.ToBase64String(alg.Sign(null, bytesToSign)) }; Validate(decodedPayload, rawSignature, recreatedSignatures); } diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index a82ca8c3a..bbd8a1e92 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs @@ -418,11 +418,16 @@ public void Encode_With_Symmetrical_WebKey_From_WebKey_Set_Should_Return_Token() var token = JwtBuilder.Create() .WithJsonWebKeySet(TestData.JsonWebKeySet) .WithJsonWebKey("OCT-Test-Key", JwtAlgorithmName.HS256) - .AddClaim("Data", TestData.Customer) - .Encode(); + .Encode(TestData.Customer); - token.Should() - .Be("eyJraWQiOiJPQ1QtVGVzdC1LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJEYXRhIjp7IkZpcnN0TmFtZSI6Ikplc3VzIiwiQWdlIjozM319.GFNIchXNoTLYvKT2mvO_s1_MBW-aBSfkQqxHWp7L-wo"); + token.Should().NotBeNullOrEmpty(); + + var decoded = JwtBuilder.Create() + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .Decode(token); + + decoded.Should() + .BeEquivalentTo(TestData.Customer); } #if NETSTANDARD2_0 || NET6_0_OR_GREATER From 5b3c3097c3b610fb712ab4ef0bf3fbe737435737 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 21:12:20 +0200 Subject: [PATCH 26/47] rename the property --- src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 2 +- src/JWT/Jwk/JwtWebKey.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index 290b86dba..7b5b7221d 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -59,7 +59,7 @@ private IJwtAlgorithm CreateECDSAAlgorithm(JwtDecoderContext context) X = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurveX), Y = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurveY) }, - D = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.EllipticCurvePrivateKey) + D = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.D) }; var key = ECDsa.Create(parameters); diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index f7231185d..95c2457b3 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -90,13 +90,15 @@ public JwtWebKey() public string EllipticCurveY { get; set; } /// - /// The "d" parameter. Represents a private key for the Elliptic Curve algorithm. ("kty") must be "EC" + /// The "d" parameter. If ("kty") is "EC" then it represents a + /// private key for the Elliptic Curve algorithm. If ("kty") is + /// "RSA" then it represents a private exponent parameter value /// [JsonProperty("d")] #if MODERN_DOTNET [JsonPropertyName("d")] #endif - public string EllipticCurvePrivateKey { get; set; } + public string D { get; set; } /// /// The "k" (key value) parameter contains the value of the symmetric (or other single-valued) key. It is represented as the base64url From eac7e11e5da60501cf15953de573bacd5d29cf55 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 21:40:36 +0200 Subject: [PATCH 27/47] extend JWK definition --- src/JWT/Jwk/JwtWebKey.cs | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index 95c2457b3..4022bca73 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -59,6 +59,51 @@ public JwtWebKey() #endif public string Exponent { get; set; } + /// + /// The "p" parameter which represents a First Prime Factor for RSA algorithms + /// + [JsonProperty("p")] +#if MODERN_DOTNET + [JsonPropertyName("p")] +#endif + public string FirstPrimeFactor { get; set; } + + /// + /// The "q" parameter which represents a Second Prime Factor exponent for RSA algorithms + /// + [JsonProperty("q")] +#if MODERN_DOTNET + [JsonPropertyName("q")] +#endif + public string SecondPrimeFactor { get; set; } + + /// + /// The "dp" parameter which represents a First Factor CRT Exponent for RSA algorithms + /// + [JsonProperty("dp")] +#if MODERN_DOTNET + [JsonPropertyName("dp")] +#endif + public string FirstFactorCRTExponent { get; set; } + + /// + /// The "dq" parameter which represents a Second Factor CRT Exponent for RSA algorithms + /// + [JsonProperty("dq")] +#if MODERN_DOTNET + [JsonPropertyName("dq")] +#endif + public string SecondFactorCRTExponent { get; set; } + + /// + /// The "qi" parameter which represents a First CRT Coefficient for RSA algorithms + /// + [JsonProperty("qi")] +#if MODERN_DOTNET + [JsonPropertyName("qi")] +#endif + public string FirstCRTCoefficient { get; set; } + /// /// The "crv" (curve) parameter identifies the cryptographic curve used with the key. RFC7518 defines the following valid values: /// "P-256", "P-384", "P-521" ("kty") must be "EC" From 564d4e16a22509100d97e96776853de9bc73afe8 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 21:43:38 +0200 Subject: [PATCH 28/47] allow to encode with a key from Json Web Key set with RSA algorithm --- src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 55 ++++++++++++++++--- .../Builder/JwtBuilderEncodeTests.cs | 18 ++++++ tests/JWT.Tests.Common/Models/TestData.cs | 2 +- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index 7b5b7221d..d81b9d03b 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -33,17 +33,13 @@ public IJwtAlgorithm Create(JwtDecoderContext context) private IJwtAlgorithm CreateRSAAlgorithm(JwtDecoderContext context) { - var rsaParameters = new RSAParameters - { - Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Modulus), - Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Exponent) - }; - - var publicKey = RSA.Create(); + var publicKey = CreateRSAPublicKey(); - publicKey.ImportParameters(rsaParameters); + var privateKey = CreateRSAPrivateKey(); - var algorithmFactory = new RSAlgorithmFactory(publicKey); + var algorithmFactory = privateKey == null + ? new RSAlgorithmFactory(publicKey) + : new RSAlgorithmFactory(publicKey, privateKey); return algorithmFactory.Create(context); } @@ -84,6 +80,47 @@ private IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) return algorithmFactory.Create(context); } + private RSA CreateRSAPublicKey() + { + var rsaParameters = new RSAParameters + { + Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Modulus), + Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Exponent) + }; + + var key = RSA.Create(); + + key.ImportParameters(rsaParameters); + + return key; + } + + private RSA CreateRSAPrivateKey() + { + var firstPrimeFactor = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstPrimeFactor); + + if (firstPrimeFactor == null) + return null; + + var rsaParameters = new RSAParameters + { + Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Modulus), + Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Exponent), + P = firstPrimeFactor, + Q = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SecondPrimeFactor), + D = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.D), + DP = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstFactorCRTExponent), + DQ = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SecondFactorCRTExponent), + InverseQ = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstCRTCoefficient) + }; + + var key = RSA.Create(); + + key.ImportParameters(rsaParameters); + + return key; + } + #if NETSTANDARD2_0 || NET6_0_OR_GREATER private ECCurve GetEllipticCurve() { diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index bbd8a1e92..066197046 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs @@ -450,6 +450,24 @@ public void Encode_With_Elliptic_Curve_WebKey_From_WebKey_Set_Should_Return_Toke } #endif + [TestMethod] + public void Encode_With_RSA_WebKey_From_WebKey_Set_Should_Return_Token() + { + var token = JwtBuilder.Create() + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .WithJsonWebKey("CFAEAE2D650A6CA9862575DE54371EA980643849", JwtAlgorithmName.RS256) + .Encode(TestData.Customer); + + token.Should().NotBeNullOrEmpty(); + + var decoded = JwtBuilder.Create() + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .Decode(token); + + decoded.Should() + .BeEquivalentTo(TestData.Customer); + } + private sealed class CustomFactory : IAlgorithmFactory { public IJwtAlgorithm Create(JwtDecoderContext context) => diff --git a/tests/JWT.Tests.Common/Models/TestData.cs b/tests/JWT.Tests.Common/Models/TestData.cs index a8a1be224..5d2e19731 100644 --- a/tests/JWT.Tests.Common/Models/TestData.cs +++ b/tests/JWT.Tests.Common/Models/TestData.cs @@ -68,7 +68,7 @@ public class TestDataSystemTextSerializerDecorated public const string TokenByAsymmetricAlgorithm = "eyJraWQiOiJDRkFFQUUyRDY1MEE2Q0E5ODYyNTc1REU1NDM3MUVBOTgwNjQzODQ5IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoyMTQ3NDgzNjQ4LCJGaXJzdE5hbWUiOiJKZXN1cyIsIkFnZSI6MzN9.ZeGfWN3kBHZLiSh4jzzn6kx7F6lNu5OsowZW0Sv-_wpSgQO2_QXFUPLx23wm4J9rjMGQlSksEtCLd_X3iiBOBLbxAUWzdj59iJIAh485unZj12sBJ7KHDVsOMc6DcSJdwRo9S9yiJ_RJ57R-dn4uRdZTBXBZHrrmb35UjaAG6hFfu5d1Ap4ZjLxqDJGl0Wo4j5l6vR8HFpmiFHvqPQ4apjqkBGnitJ7oghbeRX0SIVNSkXbBDp3i9pC-hxzs2oHZC9ys0rJlfpxLls3MV4oQbQ7m6W9MrwwsdObJHI7PiTNfObLKdgySi6WkQS7rwXVz0DqRa8TXv8_USkvhsyGLMQ"; - public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\"},{\"kty\":\"EC\",\"kid\":\"EC-Test-Key\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0\",\"y\":\"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps\",\"d\":\"0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo\"},{\"kty\":\"oct\",\"kid\":\"OCT-Test-Key\",\"use\":\"sig\",\"k\":\"R1FEc3RjS3N4ME5IalBPdVhPWWc1TWJlSjFYVDB1Rml3RFZ2VkJyaw\"}]}"; + public const string JsonWebKeySet = "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"CFAEAE2D650A6CA9862575DE54371EA980643849\",\"use\":\"sig\",\"n\":\"uYTPtHCIztKC3MUDxnZ0ktGVSQ0jVbD5rYl4pki4RCD3M22d-TklmvTyPj0SM7a_8o7cI05QhEuBI8hKCfC2CEJhlS3WFeVC0vwsl1aYFqQ3Ykr-kDsAdqjL95ioj3JmiscvqKOM34oQahpAgukJ7Kcr1BT2Ylk8fOgKcN7t1qgURNx0Pj4zJ4w0p1nT2gLG--bYutUVPvamI9wcMQyUesZwGmM9UUpMRzsOPk8vv7TbTm62Zkx-5rFUaVe5DFNUIMg92NvyU0392FFNCwptSflidHDG1ayCwL1ZTkJ0Z9yJXCNSzi_3ulxMhE-bVcpr_EuRKCYxn9qPFZ07Bd77bQ\",\"e\":\"AQAB\",\"p\":\"2Mrzzbb8Gh7aoW0YXdtdO7WEZ7-pOvbxdp4Qw8sp8dF5cF5vss3I2FoJ9kssy_DsUsreBUhD0HKrADBus7BHKXp7Q_9hhu1nAJxpng255cUfngVD9k1xQdfWEHCeWrr7XJHcplTkh4ysH4nWK-8S-RoCpiuphkJJqVxPzDaY1-M\",\"q\":\"2xHwflmaMbNs9dXi3wx10SyG5KQJeRIXlKkhlUYlAU-7598AdmTiUPHfhj4WDRCmcJGHjSWqdiuQuwmRYsBXRhtk7XjGAjcefloSpXSR9G-tpVFuIthBU337g2pK1o8z_29LKiWZvcytgxQLEWwGIyduj2I9BoDw1jgFmVd_IG8\",\"d\":\"lJyKkl3vidZilB2Sh6IOgio371wB6Twq1lQgfPwV-CV8QQtXl_SqZjY_85GSijCkFNdSC0pJ_6BIY_SnMs1L1NPwPcOJEuMjo8X0porsrH6CC1BOGhXZqjRPqBj3NmoLMLKdP_c7-zorKgO7l-K8W4IS_wKH2ILpjJmI-5_pYKDI66CD9wbgrpKXjnajboDDEMGp64cT3lJnUsr_DucmYIx1VD646ErMiAxwr1qiBg9jpTSjRuubJJhnzN_j192RkqCOgc0j0SE1Ww0AZTVct4IvrvE3S5fnE1apRlzEAfrfMVn2rbrUTRgIKBpTWGY_m0weQzMmisIBiauUmi2nFQ\",\"dp\":\"b9n-ghO37G4g1QqpeLtWVhkoEDNFyANiv5V8BtjKclZmdoBy1ujviBikbSuKGErcUzcR593KB0EyUu2qIBGCFbd447NeiTPxYdJRd9eTIyZaUrhawThhh9wpOOAyA5PXXoJvOm4wXnNI1xjRpGc7_cPavAto8rk-sh_LmAxPPYs\",\"dq\":\"b2l2N6v2IWSw-22lje5WVOUiTVGnh61N1MsXS0V7OGmGlOvy3kN8XdJE7Y7RxB89pm480-neAW8ykgzRpblQKVVxRNxxR1sk5PmGFiNsvzW0yCjbrFjzEDU4HqOGIAyAU14UigDJaZ-YdttQrbGUhXheYAmEI7SbxzaCknPPMX0\",\"qi\":\"SpRpqI-Z4g3jMbb0iE0oD-FAUaBXGp00DjKVbeYH8WQl2rVGFkspFYeN69u3ZFUL3JJd4rCF6zbuLq6iyDJq_F-Jo4zSzXChepr_dSEH1TszaA6imdqFyj3pjOT_ZXNK4YPCRijRM3fy8GdNybZDQljL1djY8D1YK3CWEtKuogs\"},{\"kty\":\"EC\",\"kid\":\"EC-Test-Key\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0\",\"y\":\"SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps\",\"d\":\"0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo\"},{\"kty\":\"oct\",\"kid\":\"OCT-Test-Key\",\"use\":\"sig\",\"k\":\"R1FEc3RjS3N4ME5IalBPdVhPWWc1TWJlSjFYVDB1Rml3RFZ2VkJyaw\"}]}"; public static readonly IDictionary DictionaryPayload = new Dictionary { From ea878af7990679c0fd2162a4d96471a3de76dee1 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 21:53:22 +0200 Subject: [PATCH 29/47] polish - remove code duplications --- src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 35 ++++++-------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index d81b9d03b..90aa628c1 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -33,9 +33,9 @@ public IJwtAlgorithm Create(JwtDecoderContext context) private IJwtAlgorithm CreateRSAAlgorithm(JwtDecoderContext context) { - var publicKey = CreateRSAPublicKey(); + var publicKey = CreateRSAKey(false); - var privateKey = CreateRSAPrivateKey(); + var privateKey = CreateRSAKey(true); var algorithmFactory = privateKey == null ? new RSAlgorithmFactory(publicKey) @@ -80,38 +80,23 @@ private IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) return algorithmFactory.Create(context); } - private RSA CreateRSAPublicKey() - { - var rsaParameters = new RSAParameters - { - Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Modulus), - Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Exponent) - }; - - var key = RSA.Create(); - - key.ImportParameters(rsaParameters); - - return key; - } - - private RSA CreateRSAPrivateKey() + private RSA CreateRSAKey(bool privateKey) { var firstPrimeFactor = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstPrimeFactor); - if (firstPrimeFactor == null) + if (privateKey && firstPrimeFactor == null) return null; var rsaParameters = new RSAParameters { Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Modulus), Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Exponent), - P = firstPrimeFactor, - Q = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SecondPrimeFactor), - D = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.D), - DP = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstFactorCRTExponent), - DQ = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SecondFactorCRTExponent), - InverseQ = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstCRTCoefficient) + P = privateKey ? firstPrimeFactor : null, + Q = privateKey ? JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SecondPrimeFactor) : null, + D = privateKey ? JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.D) : null, + DP = privateKey ? JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstFactorCRTExponent) : null, + DQ = privateKey ? JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SecondFactorCRTExponent) : null, + InverseQ = privateKey ? JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstCRTCoefficient) : null }; var key = RSA.Create(); From 884844502fb4f1432dab57e633dec0d840830edf Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 22:00:15 +0200 Subject: [PATCH 30/47] add a note to the changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1210db4f7..2359eefbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Unreleased +- Added JSON Web keys support + +It's possible to use JWKS with +```cs +var algorithmFactory = new JwtJsonWebKeySetAlgorithmFactory(webkeySetJsonString, serializer); + +var decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithmFactory); + +var json = decoder.Decode(token); +``` +or using the fluent builder API: +```cs +var decoded = JwtBuilder.Create() + .WithJsonWebKeySet(webkeySetJsonString) + .Decode(token); +``` + # 10.1.1 - Made ctor of ValidationParameters public, set default values for boolean properties to true From 3f08528c535a7ccba7555c029540091926cffd44 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Sun, 4 Feb 2024 22:02:24 +0200 Subject: [PATCH 31/47] bump version --- CHANGELOG.md | 2 ++ src/JWT/JWT.csproj | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2359eefbe..d73c7c544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +# 10.2.0 + - Added JSON Web keys support It's possible to use JWKS with diff --git a/src/JWT/JWT.csproj b/src/JWT/JWT.csproj index bc3413314..bf75d1b78 100644 --- a/src/JWT/JWT.csproj +++ b/src/JWT/JWT.csproj @@ -25,7 +25,7 @@ - 10.1.1 + 10.2.0 10.0.0.0 10.0.0.0 From 7e09d4b11f2a2a6471e7197eb665f586772e9c00 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Wed, 1 May 2024 11:34:08 +0300 Subject: [PATCH 32/47] fix build replace MODERN_DOTNET which is not presented any more --- src/JWT/Jwk/JwtWebKey.cs | 32 ++++++++++++++++---------------- src/JWT/Jwk/JwtWebKeySet.cs | 6 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index 4022bca73..c16b5af5c 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER using System.Text.Json.Serialization; #endif @@ -12,7 +12,7 @@ namespace JWT.Jwk /// public sealed class JwtWebKey { -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [System.Text.Json.Serialization.JsonConstructor] public JwtWebKey() { @@ -25,7 +25,7 @@ public JwtWebKey() /// Valid values are "EC" (Elliptic Curve), "RSA" and "oct" (octet sequence used to represent symmetric keys) /// [JsonProperty("kty")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("kty")] #endif public string KeyType { get; set; } @@ -34,7 +34,7 @@ public JwtWebKey() /// The "kid" parameter which defines key id /// [JsonProperty("kid")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("kid")] #endif public string KeyId { get; set; } @@ -44,7 +44,7 @@ public JwtWebKey() /// ("kty") must be "RSA" /// [JsonProperty("n")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("n")] #endif public string Modulus { get; set; } @@ -54,7 +54,7 @@ public JwtWebKey() /// ("kty") must be "RSA" /// [JsonProperty("e")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("e")] #endif public string Exponent { get; set; } @@ -63,7 +63,7 @@ public JwtWebKey() /// The "p" parameter which represents a First Prime Factor for RSA algorithms /// [JsonProperty("p")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("p")] #endif public string FirstPrimeFactor { get; set; } @@ -72,7 +72,7 @@ public JwtWebKey() /// The "q" parameter which represents a Second Prime Factor exponent for RSA algorithms /// [JsonProperty("q")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("q")] #endif public string SecondPrimeFactor { get; set; } @@ -81,7 +81,7 @@ public JwtWebKey() /// The "dp" parameter which represents a First Factor CRT Exponent for RSA algorithms /// [JsonProperty("dp")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("dp")] #endif public string FirstFactorCRTExponent { get; set; } @@ -90,7 +90,7 @@ public JwtWebKey() /// The "dq" parameter which represents a Second Factor CRT Exponent for RSA algorithms /// [JsonProperty("dq")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("dq")] #endif public string SecondFactorCRTExponent { get; set; } @@ -99,7 +99,7 @@ public JwtWebKey() /// The "qi" parameter which represents a First CRT Coefficient for RSA algorithms /// [JsonProperty("qi")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("qi")] #endif public string FirstCRTCoefficient { get; set; } @@ -109,7 +109,7 @@ public JwtWebKey() /// "P-256", "P-384", "P-521" ("kty") must be "EC" /// [JsonProperty("crv")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("crv")] #endif public string EllipticCurveType { get; set; } @@ -119,7 +119,7 @@ public JwtWebKey() /// the octet string representation of the coordinate. ("kty") must be "EC" /// [JsonProperty("x")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("x")] #endif public string EllipticCurveX { get; set; } @@ -129,7 +129,7 @@ public JwtWebKey() /// the octet string representation of the coordinate. ("kty") must be "EC" /// [JsonProperty("y")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("y")] #endif public string EllipticCurveY { get; set; } @@ -140,7 +140,7 @@ public JwtWebKey() /// "RSA" then it represents a private exponent parameter value /// [JsonProperty("d")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("d")] #endif public string D { get; set; } @@ -150,7 +150,7 @@ public JwtWebKey() /// encoding of the octet sequence containing the key value. ("kty") must be "oct" /// [JsonProperty("k")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("k")] #endif public string SymmetricKey { get; set; } diff --git a/src/JWT/Jwk/JwtWebKeySet.cs b/src/JWT/Jwk/JwtWebKeySet.cs index 36efcd928..6623185d8 100644 --- a/src/JWT/Jwk/JwtWebKeySet.cs +++ b/src/JWT/Jwk/JwtWebKeySet.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER using System.Text.Json.Serialization; #endif @@ -14,7 +14,7 @@ namespace JWT.Jwk /// public sealed class JwtWebKeySet { -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [System.Text.Json.Serialization.JsonConstructor] public JwtWebKeySet() { @@ -23,7 +23,7 @@ public JwtWebKeySet() #endif [JsonProperty("keys")] -#if MODERN_DOTNET +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("keys")] #endif public IEnumerable Keys { get; set; } = null!; From e5f9f61537bcfb8753d5e1f6635c273ccfe5a271 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:04:13 -0700 Subject: [PATCH 33/47] Update JwtWebKeyPropertyValuesEncoder.cs --- src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs b/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs index 47ea57053..bbe55ed57 100644 --- a/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs +++ b/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs @@ -1,59 +1,60 @@ using System; -namespace JWT.Jwk; - -/// -/// Based on Microsoft.AspNetCore.WebUtilities.WebEncoders -/// -internal static class JwtWebKeyPropertyValuesEncoder +namespace JWT.Jwk { - public static byte[] Base64UrlDecode(string input) + /// + /// Based on Microsoft.AspNetCore.WebUtilities.WebEncoders + /// + internal static class JwtWebKeyPropertyValuesEncoder { - 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) + public static byte[] Base64UrlDecode(string input) { - var symbol = input[i]; - - switch (symbol) + if (input is null) + return null; + + var paddingCharsCount = GetNumBase64PaddingCharsToAddForDecode(input.Length); + var buffer = new char[input.Length + paddingCharsCount]; + + for (var i = 0; i < input.Length; ++i) { - case '-': - buffer[i] = '+'; - break; - case '_': - buffer[i] = '/'; - break; + char ch = DecodeCharacter(input[i]); + buffer[i] = ch; + } + + for (var i = input.Length; i < buffer.Length; ++i) + { + buffer[i] = '='; + } + + return Convert.FromBase64CharArray(buffer, 0, buffer.Length); + } + + private static int GetNumBase64PaddingCharsToAddForDecode(int length) + { + switch (length % 4) + { + case 0: + return 0; + case 2: + return 2; + case 3: + return 1; default: - buffer[i] = symbol; - break; + throw new ArgumentOutOfRangeException (nameof(length), $"Malformed input: {length} is an invalid input length."); } } - - 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) + + private static char DecodeCharacter(char ch) { - case 0: - return 0; - case 2: - return 2; - case 3: - return 1; - default: - throw new FormatException($"Malformed input: {inputLength} is an invalid input length."); + switch (ch) + { + case '-': + return '+'; + case '_': + return '/'; + default: + return ch; + } } } -} \ No newline at end of file +} From a8c1651a54671c12b647fce15121527ed134e5e6 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:09:54 -0700 Subject: [PATCH 34/47] Update JwtBuilderEncodeTests.cs --- .../Builder/JwtBuilderEncodeTests.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index 066197046..bbaa542b8 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs @@ -416,18 +416,18 @@ public void Encode_Test_Bug438() public void Encode_With_Symmetrical_WebKey_From_WebKey_Set_Should_Return_Token() { var token = JwtBuilder.Create() - .WithJsonWebKeySet(TestData.JsonWebKeySet) - .WithJsonWebKey("OCT-Test-Key", JwtAlgorithmName.HS256) - .Encode(TestData.Customer); + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .WithJsonWebKey("OCT-Test-Key", JwtAlgorithmName.HS256) + .Encode(TestData.Customer); token.Should().NotBeNullOrEmpty(); var decoded = JwtBuilder.Create() - .WithJsonWebKeySet(TestData.JsonWebKeySet) - .Decode(token); + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .Decode(token); decoded.Should() - .BeEquivalentTo(TestData.Customer); + .BeEquivalentTo(TestData.Customer); } #if NETSTANDARD2_0 || NET6_0_OR_GREATER @@ -435,18 +435,18 @@ public void Encode_With_Symmetrical_WebKey_From_WebKey_Set_Should_Return_Token() public void Encode_With_Elliptic_Curve_WebKey_From_WebKey_Set_Should_Return_Token() { var token = JwtBuilder.Create() - .WithJsonWebKeySet(TestData.JsonWebKeySet) - .WithJsonWebKey("EC-Test-Key", JwtAlgorithmName.ES256) - .Encode(TestData.Customer); + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .WithJsonWebKey("EC-Test-Key", JwtAlgorithmName.ES256) + .Encode(TestData.Customer); token.Should().NotBeNullOrEmpty(); var decoded = JwtBuilder.Create() - .WithJsonWebKeySet(TestData.JsonWebKeySet) - .Decode(token); + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .Decode(token); decoded.Should() - .BeEquivalentTo(TestData.Customer); + .BeEquivalentTo(TestData.Customer); } #endif @@ -454,18 +454,18 @@ public void Encode_With_Elliptic_Curve_WebKey_From_WebKey_Set_Should_Return_Toke public void Encode_With_RSA_WebKey_From_WebKey_Set_Should_Return_Token() { var token = JwtBuilder.Create() - .WithJsonWebKeySet(TestData.JsonWebKeySet) - .WithJsonWebKey("CFAEAE2D650A6CA9862575DE54371EA980643849", JwtAlgorithmName.RS256) - .Encode(TestData.Customer); + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .WithJsonWebKey("CFAEAE2D650A6CA9862575DE54371EA980643849", JwtAlgorithmName.RS256) + .Encode(TestData.Customer); token.Should().NotBeNullOrEmpty(); var decoded = JwtBuilder.Create() - .WithJsonWebKeySet(TestData.JsonWebKeySet) - .Decode(token); + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .Decode(token); decoded.Should() - .BeEquivalentTo(TestData.Customer); + .BeEquivalentTo(TestData.Customer); } private sealed class CustomFactory : IAlgorithmFactory From 90f02d829f5fde0a78fca2e6a91045e2b48b5f8a Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:13:34 -0700 Subject: [PATCH 35/47] Update JwtDecoderTests.cs --- tests/JWT.Tests.Common/JwtDecoderTests.cs | 95 +++++++---------------- 1 file changed, 30 insertions(+), 65 deletions(-) diff --git a/tests/JWT.Tests.Common/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index c39457d52..46a6cc09d 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -489,6 +489,7 @@ public void DecodeToObject_Should_Throw_Exception_On_Expired_Claim() public void DecodeToObject_Should_Decode_Token_On_Exp_Claim_After_Year2038() { const string key = TestData.Secret; + var dateTimeProvider = new UtcDateTimeProvider(); var serializer = CreateSerializer(); var validator = new JwtValidator(serializer, dateTimeProvider); @@ -533,7 +534,6 @@ public void DecodeToObject_Should_Throw_Exception_Before_NotBefore_Becomes_Valid public void DecodeToObject_Should_Decode_Token_After_NotBefore_Becomes_Valid() { var dateTimeProvider = new UtcDateTimeProvider(); - const string key = TestData.Secret; var serializer = CreateSerializer(); var validator = new JwtValidator(serializer, new UtcDateTimeProvider()); @@ -545,9 +545,9 @@ public void DecodeToObject_Should_Decode_Token_After_NotBefore_Becomes_Valid() var nbf = UnixEpoch.GetSecondsSince(now); var encoder = new JwtEncoder(TestData.HMACSHA256Algorithm, serializer, urlEncoder); - var token = encoder.Encode(new { nbf }, key); + var token = encoder.Encode(new { nbf }, TestData.Secret); - var dic = decoder.DecodeToObject>(token, key, verify: true); + var dic = decoder.DecodeToObject>(token, TestData.Secret, verify: true); dic.Should() .Contain("nbf", nbf); @@ -560,7 +560,6 @@ public void DecodeToObject_Should_Throw_Exception_On_Null_NotBefore_Claim() var serializer = CreateSerializer(); var validator = new JwtValidator(serializer, new UtcDateTimeProvider()); - var urlEncoder = new JwtBase64UrlEncoder(); var decoder = new JwtDecoder(serializer, validator, urlEncoder, TestData.HMACSHA256Algorithm); @@ -578,22 +577,15 @@ public void DecodeToObject_Should_Throw_Exception_On_Null_NotBefore_Claim() public void Should_Decode_With_Json_Web_Keys_RSA() { 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); + customer.Should() + .BeEquivalentTo(TestData.Customer); } #if NETSTANDARD2_0 || NET6_0_OR_GREATER @@ -601,114 +593,87 @@ public void Should_Decode_With_Json_Web_Keys_RSA() public void Should_Decode_With_Json_Web_Keys_EC() { var ecDsa = ECDsa.Create(TestData.EllipticCurvesParameters); - var token = JwtBuilder.Create() - .WithAlgorithm(new ES256Algorithm(ecDsa, ecDsa)) - .AddHeader(HeaderName.KeyId, "EC-Test-Key") - .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) - .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) - .Encode(); + .WithAlgorithm(new ES256Algorithm(ecDsa, ecDsa)) + .AddHeader(HeaderName.KeyId, "EC-Test-Key") + .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) + .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) + .Encode(); 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(token); - Assert.IsNotNull(customer); - - customer - .Should() - .BeEquivalentTo(TestData.Customer); + customer.Should() + .BeEquivalentTo(TestData.Customer); } #endif [TestMethod] public void Should_Decode_With_Json_Web_Keys_() { - const string key = TestData.Secret; - var token = JwtBuilder.Create() - .WithAlgorithm(new HMACSHA256Algorithm()) - .WithSecret(key) - .AddHeader(HeaderName.KeyId, "OCT-Test-Key") - .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) - .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) - .Encode(); + .WithAlgorithm(new HMACSHA256Algorithm()) + .WithSecret(TestData.Secret) + .AddHeader(HeaderName.KeyId, "OCT-Test-Key") + .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) + .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) + .Encode(); 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(token); - Assert.IsNotNull(customer); - - customer - .Should() - .BeEquivalentTo(TestData.Customer); + customer.Should() + .BeEquivalentTo(TestData.Customer); } [TestMethod] public void DecodeToObject_With_Json_Web_keys_Should_Throw_Exception_If_Key_Is_Missing_In_Token() { 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); Action action = () => decoder.DecodeToObject(TestData.Token); action.Should() - .Throw() - .WithMessage("The key id is missing in the token header"); + .Throw() + .WithMessage("The key id is missing in the token header"); } [TestMethod] public void DecodeToObject_With_Json_Web_keys_Should_Throw_Exception_If_Key_Is_Not_In_Collection() { 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); - const string key = TestData.Secret; - var token = JwtBuilder.Create() - .WithAlgorithm(TestData.HMACSHA256Algorithm) - .WithSecret(key) - .AddHeader(HeaderName.KeyId, "42") - .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) - .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) - .Encode(); + .WithAlgorithm(TestData.HMACSHA256Algorithm) + .WithSecret(TestData.Secret) + .AddHeader(HeaderName.KeyId, "42") + .AddClaim(nameof(TestData.Customer.FirstName), TestData.Customer.FirstName) + .AddClaim(nameof(TestData.Customer.Age), TestData.Customer.Age) + .Encode(); Action action = () => decoder.DecodeToObject(token); action.Should() - .Throw() - .WithMessage("The key id is not presented in the JSON Web key set"); + .Throw() + .WithMessage("The key id is not presented in the JSON Web key set"); } private static IJsonSerializer CreateSerializer() => From 80eb5293c492c1e1f5104538c5e8b48302daf320 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:14:51 -0700 Subject: [PATCH 36/47] Update JwtValidator.cs --- src/JWT/JwtValidator.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/JWT/JwtValidator.cs b/src/JWT/JwtValidator.cs index 1f417abfa..2f633b985 100644 --- a/src/JWT/JwtValidator.cs +++ b/src/JWT/JwtValidator.cs @@ -99,9 +99,9 @@ public void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm a var rawSignature = Convert.ToBase64String(decodedSignature); // the signatures re-created by the algorithm, with the leading = - var recreatedSignatures = keys != null - ? keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray() - : new string[] { Convert.ToBase64String(alg.Sign(null, bytesToSign)) }; + var recreatedSignatures = keys is not null ? + keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray() + [Convert.ToBase64String(alg.Sign(null, bytesToSign))]; Validate(decodedPayload, rawSignature, recreatedSignatures); } @@ -284,8 +284,6 @@ private Exception ValidateNbfClaim(IReadOnlyPayloadDictionary payloadData, doubl return null; } - - private static bool AllKeysHaveValues(byte[][] keys) { if (keys is null) From d48dee145a0c31af4e018081d8b2ed9e7507f571 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:15:52 -0700 Subject: [PATCH 37/47] Update JwtWebKeySet.cs --- src/JWT/Jwk/JwtWebKeySet.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/JWT/Jwk/JwtWebKeySet.cs b/src/JWT/Jwk/JwtWebKeySet.cs index 6623185d8..9038931a2 100644 --- a/src/JWT/Jwk/JwtWebKeySet.cs +++ b/src/JWT/Jwk/JwtWebKeySet.cs @@ -15,10 +15,9 @@ namespace JWT.Jwk public sealed class JwtWebKeySet { #if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER - [System.Text.Json.Serialization.JsonConstructor] + [JsonConstructor] public JwtWebKeySet() { - } #endif @@ -28,4 +27,4 @@ public JwtWebKeySet() #endif public IEnumerable Keys { get; set; } = null!; } -} \ No newline at end of file +} From 1a3a717ffb341a8ef70e6b2e79848f8575dbf4f8 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:16:07 -0700 Subject: [PATCH 38/47] Update JwtWebKeySet.cs --- src/JWT/Jwk/JwtWebKeySet.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JWT/Jwk/JwtWebKeySet.cs b/src/JWT/Jwk/JwtWebKeySet.cs index 9038931a2..9e558d5e3 100644 --- a/src/JWT/Jwk/JwtWebKeySet.cs +++ b/src/JWT/Jwk/JwtWebKeySet.cs @@ -1,4 +1,3 @@ - using System.Collections.Generic; using Newtonsoft.Json; From 89cc32fa2c61268077bb1dffb4effd18acac770b Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:33:16 -0700 Subject: [PATCH 39/47] Update JwtValidator.cs --- src/JWT/JwtValidator.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/JWT/JwtValidator.cs b/src/JWT/JwtValidator.cs index 2f633b985..aa57b33b6 100644 --- a/src/JWT/JwtValidator.cs +++ b/src/JWT/JwtValidator.cs @@ -88,6 +88,11 @@ public void Validate(string decodedPayload, IAsymmetricAlgorithm alg, byte[] byt throw ex; } + /// + /// + /// + /// + /// public void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm alg, byte[] bytesToSign, byte[] decodedSignature) { if (alg.Key == null && keys is null) @@ -100,7 +105,7 @@ public void Validate(byte[][] keys, string decodedPayload, ISymmetricAlgorithm a // the signatures re-created by the algorithm, with the leading = var recreatedSignatures = keys is not null ? - keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray() + keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray() : [Convert.ToBase64String(alg.Sign(null, bytesToSign))]; Validate(decodedPayload, rawSignature, recreatedSignatures); From 37aafbbfc176660582599039431d0f3fc17eb733 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Thu, 2 May 2024 16:57:00 -0700 Subject: [PATCH 40/47] Update JwtWebKeySet.cs --- src/JWT/Jwk/JwtWebKeySet.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/JWT/Jwk/JwtWebKeySet.cs b/src/JWT/Jwk/JwtWebKeySet.cs index 9e558d5e3..23ce3bdfb 100644 --- a/src/JWT/Jwk/JwtWebKeySet.cs +++ b/src/JWT/Jwk/JwtWebKeySet.cs @@ -13,13 +13,6 @@ namespace JWT.Jwk /// public sealed class JwtWebKeySet { -#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER - [JsonConstructor] - public JwtWebKeySet() - { - } -#endif - [JsonProperty("keys")] #if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER [JsonPropertyName("keys")] From a66322b16dfd0c295f072f1d638756aa2efbd93c Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Fri, 3 May 2024 09:37:31 +0300 Subject: [PATCH 41/47] explicit namespace --- src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs b/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs index ffe2bb843..33f8f81b5 100644 --- a/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs +++ b/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs @@ -1,6 +1,7 @@ -namespace JWT.Jwk; - -public interface IJwtWebKeysCollectionFactory +namespace JWT.Jwk { - JwtWebKeysCollection CreateKeys(); + public interface IJwtWebKeysCollectionFactory + { + JwtWebKeysCollection CreateKeys(); + } } \ No newline at end of file From 6dd5743f13f3f1577824d5439aaf9a641cc6be0e Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Fri, 3 May 2024 09:46:07 +0300 Subject: [PATCH 42/47] to expression body --- .../Jwk/JwtJsonWebKeySetAlgorithmFactory.cs | 20 +++++-------------- src/JWT/Jwk/JwtWebKeysCollection.cs | 10 ++-------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs index bf2984a03..521eb9e70 100644 --- a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs @@ -9,30 +9,20 @@ public sealed class JwtJsonWebKeySetAlgorithmFactory : IAlgorithmFactory { private readonly JwtWebKeysCollection _webKeysCollection; - public JwtJsonWebKeySetAlgorithmFactory(JwtWebKeysCollection webKeysCollection) - { + public JwtJsonWebKeySetAlgorithmFactory(JwtWebKeysCollection webKeysCollection) => _webKeysCollection = webKeysCollection; - } - public JwtJsonWebKeySetAlgorithmFactory(Func getJsonWebKeys) - { + public JwtJsonWebKeySetAlgorithmFactory(Func getJsonWebKeys) => _webKeysCollection = getJsonWebKeys(); - } - public JwtJsonWebKeySetAlgorithmFactory(IJwtWebKeysCollectionFactory webKeysCollectionFactory) - { + public JwtJsonWebKeySetAlgorithmFactory(IJwtWebKeysCollectionFactory webKeysCollectionFactory) => _webKeysCollection = webKeysCollectionFactory.CreateKeys(); - } - public JwtJsonWebKeySetAlgorithmFactory(string keySet, IJsonSerializer serializer) - { + public JwtJsonWebKeySetAlgorithmFactory(string keySet, IJsonSerializer serializer) => _webKeysCollection = new JwtWebKeysCollection(keySet, serializer); - } - public JwtJsonWebKeySetAlgorithmFactory(string keySet, IJsonSerializerFactory jsonSerializerFactory) - { + public JwtJsonWebKeySetAlgorithmFactory(string keySet, IJsonSerializerFactory jsonSerializerFactory) => _webKeysCollection = new JwtWebKeysCollection(keySet, jsonSerializerFactory); - } public IJwtAlgorithm Create(JwtDecoderContext context) { diff --git a/src/JWT/Jwk/JwtWebKeysCollection.cs b/src/JWT/Jwk/JwtWebKeysCollection.cs index 591dd1fcc..f5892620b 100644 --- a/src/JWT/Jwk/JwtWebKeysCollection.cs +++ b/src/JWT/Jwk/JwtWebKeysCollection.cs @@ -8,10 +8,7 @@ public class JwtWebKeysCollection : IJwtWebKeysCollection { private readonly Dictionary _keys; - public JwtWebKeysCollection(IEnumerable keys) - { - _keys = keys.ToDictionary(x => x.KeyId); - } + public JwtWebKeysCollection(IEnumerable keys) => _keys = keys.ToDictionary(x => x.KeyId); public JwtWebKeysCollection(JwtWebKeySet keySet) : this(keySet.Keys) { @@ -30,9 +27,6 @@ public JwtWebKeysCollection(string keySet, IJsonSerializerFactory jsonSerializer } - public JwtWebKey Find(string keyId) - { - return _keys.TryGetValue(keyId, out var key) ? key : null; - } + public JwtWebKey Find(string keyId) => _keys.TryGetValue(keyId, out var key) ? key : null; } } \ No newline at end of file From e7189c1d0ea24fd3f7fec12d33798e7944bb4e9f Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Fri, 3 May 2024 09:59:27 +0300 Subject: [PATCH 43/47] remove blank lines --- src/JWT/Algorithms/HMACSHA256Algorithm.cs | 4 +--- src/JWT/Algorithms/HMACSHA384Algorithm.cs | 4 +--- src/JWT/Algorithms/HMACSHA512Algorithm.cs | 2 -- src/JWT/Algorithms/HMACSHAAlgorithm.cs | 8 ++------ src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs | 6 +----- .../InvalidJsonWebKeyEllipticCurveTypeException.cs | 1 - src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs | 1 - src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 5 +---- src/JWT/Jwk/JwtWebKey.cs | 1 - src/JWT/Jwk/JwtWebKeysCollection.cs | 9 +++------ 10 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/JWT/Algorithms/HMACSHA256Algorithm.cs b/src/JWT/Algorithms/HMACSHA256Algorithm.cs index 3ac7cb303..2a6d049fa 100644 --- a/src/JWT/Algorithms/HMACSHA256Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA256Algorithm.cs @@ -9,12 +9,10 @@ public sealed class HMACSHA256Algorithm : HMACSHAAlgorithm { public HMACSHA256Algorithm() { - } internal HMACSHA256Algorithm(byte[] key) : base(key) - { - + { } /// diff --git a/src/JWT/Algorithms/HMACSHA384Algorithm.cs b/src/JWT/Algorithms/HMACSHA384Algorithm.cs index b648245f3..5c36405bf 100644 --- a/src/JWT/Algorithms/HMACSHA384Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA384Algorithm.cs @@ -9,12 +9,10 @@ public sealed class HMACSHA384Algorithm : HMACSHAAlgorithm { public HMACSHA384Algorithm() { - } internal HMACSHA384Algorithm(byte[] key) : base(key) - { - + { } /// diff --git a/src/JWT/Algorithms/HMACSHA512Algorithm.cs b/src/JWT/Algorithms/HMACSHA512Algorithm.cs index a964d159a..b944f1f91 100644 --- a/src/JWT/Algorithms/HMACSHA512Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA512Algorithm.cs @@ -9,12 +9,10 @@ public sealed class HMACSHA512Algorithm : HMACSHAAlgorithm { public HMACSHA512Algorithm() { - } internal HMACSHA512Algorithm(byte[] key) : base(key) { - } /// diff --git a/src/JWT/Algorithms/HMACSHAAlgorithm.cs b/src/JWT/Algorithms/HMACSHAAlgorithm.cs index 4a241c3fa..54d94fc2b 100644 --- a/src/JWT/Algorithms/HMACSHAAlgorithm.cs +++ b/src/JWT/Algorithms/HMACSHAAlgorithm.cs @@ -5,14 +5,10 @@ namespace JWT.Algorithms public abstract class HMACSHAAlgorithm : ISymmetricAlgorithm { protected HMACSHAAlgorithm() - { - + { } - protected HMACSHAAlgorithm(byte[] key) - { - this.Key = key; - } + protected HMACSHAAlgorithm(byte[] key) => this.Key = key; /// public abstract string Name { get; } diff --git a/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs b/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs index 2d66e900f..29783ef41 100644 --- a/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs +++ b/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs @@ -9,13 +9,9 @@ public class HMACSHAAlgorithmFactory : JwtAlgorithmFactory public HMACSHAAlgorithmFactory() { - } - public HMACSHAAlgorithmFactory(byte[] key) - { - _key = key; - } + public HMACSHAAlgorithmFactory(byte[] key) => _key = key; protected override IJwtAlgorithm Create(JwtAlgorithmName algorithm) { diff --git a/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs b/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs index fddb213bd..98cc0fe8a 100644 --- a/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs +++ b/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs @@ -7,7 +7,6 @@ public class InvalidJsonWebKeyEllipticCurveTypeException : ArgumentOutOfRangeExc public InvalidJsonWebKeyEllipticCurveTypeException(string ellipticCurveType) : base($"{ellipticCurveType} is not defined in RFC751") { - } } } \ No newline at end of file diff --git a/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs b/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs index 542fcd72b..aa8fdfe4b 100644 --- a/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs +++ b/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs @@ -7,7 +7,6 @@ public class InvalidJsonWebKeyTypeException : ArgumentOutOfRangeException public InvalidJsonWebKeyTypeException(string keyType) : base($"{keyType} is not defined in RFC7518") { - } } } \ No newline at end of file diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index 90aa628c1..af4b9ccf0 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -8,10 +8,7 @@ internal sealed class JwtJsonWebKeyAlgorithmFactory : IAlgorithmFactory { private readonly JwtWebKey _key; - public JwtJsonWebKeyAlgorithmFactory(JwtWebKey key) - { - _key = key; - } + public JwtJsonWebKeyAlgorithmFactory(JwtWebKey key) => _key = key; public IJwtAlgorithm Create(JwtDecoderContext context) { diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs index c16b5af5c..c5ad68cfa 100644 --- a/src/JWT/Jwk/JwtWebKey.cs +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -16,7 +16,6 @@ public sealed class JwtWebKey [System.Text.Json.Serialization.JsonConstructor] public JwtWebKey() { - } #endif diff --git a/src/JWT/Jwk/JwtWebKeysCollection.cs b/src/JWT/Jwk/JwtWebKeysCollection.cs index f5892620b..742a76592 100644 --- a/src/JWT/Jwk/JwtWebKeysCollection.cs +++ b/src/JWT/Jwk/JwtWebKeysCollection.cs @@ -11,20 +11,17 @@ public class JwtWebKeysCollection : IJwtWebKeysCollection public JwtWebKeysCollection(IEnumerable keys) => _keys = keys.ToDictionary(x => x.KeyId); public JwtWebKeysCollection(JwtWebKeySet keySet) : this(keySet.Keys) - { - + { } public JwtWebKeysCollection(string keySet, IJsonSerializer serializer) : this(serializer.Deserialize(keySet)) - { - + { } public JwtWebKeysCollection(string keySet, IJsonSerializerFactory jsonSerializerFactory) : this(keySet, jsonSerializerFactory.Create()) - { - + { } public JwtWebKey Find(string keyId) => _keys.TryGetValue(keyId, out var key) ? key : null; From c57a9a0cc6fbda7b1d5b681f594e1c3e0bf93e24 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Fri, 3 May 2024 10:09:01 +0300 Subject: [PATCH 44/47] pattern matching switch/case --- src/JWT/JwtDecoder.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/JWT/JwtDecoder.cs b/src/JWT/JwtDecoder.cs index 1c8e1616b..eaf0161f5 100644 --- a/src/JWT/JwtDecoder.cs +++ b/src/JWT/JwtDecoder.cs @@ -239,18 +239,21 @@ public void Validate(JwtParts jwt, params byte[][] keys) var header = DecodeHeader(jwt); var algorithm = _algFactory.Create(JwtDecoderContext.Create(header, decodedPayload, jwt)); - if (algorithm is null) - throw new ArgumentNullException(nameof(algorithm)); var bytesToSign = GetBytes(jwt.Header, '.', jwt.Payload); - if (algorithm is IAsymmetricAlgorithm asymmAlg) + switch (algorithm) { - _jwtValidator.Validate(decodedPayload, asymmAlg, bytesToSign, decodedSignature); - } - else if (algorithm is ISymmetricAlgorithm symmAlg) - { - _jwtValidator.Validate(keys, decodedPayload, symmAlg, bytesToSign, decodedSignature); + case IAsymmetricAlgorithm asymmAlg: + _jwtValidator.Validate(decodedPayload, asymmAlg, bytesToSign, decodedSignature); + break; + + case ISymmetricAlgorithm symmAlg: + _jwtValidator.Validate(keys, decodedPayload, symmAlg, bytesToSign, decodedSignature); + break; + + case null: + throw new ArgumentNullException(nameof(algorithm)); } } From 58e9a992f20db1dd3e150be75f8c1e15b8cddd7a Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Fri, 3 May 2024 10:17:28 +0300 Subject: [PATCH 45/47] formatting --- src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs index af4b9ccf0..f25d26292 100644 --- a/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -34,9 +34,7 @@ private IJwtAlgorithm CreateRSAAlgorithm(JwtDecoderContext context) var privateKey = CreateRSAKey(true); - var algorithmFactory = privateKey == null - ? new RSAlgorithmFactory(publicKey) - : new RSAlgorithmFactory(publicKey, privateKey); + var algorithmFactory = privateKey == null ? new RSAlgorithmFactory(publicKey) : new RSAlgorithmFactory(publicKey, privateKey); return algorithmFactory.Create(context); } From a80e41e5ef46079d6235221d41b0da4e6ee03dd3 Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Fri, 3 May 2024 10:25:26 +0300 Subject: [PATCH 46/47] seal the class --- src/JWT/Jwk/JwtWebKeysCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT/Jwk/JwtWebKeysCollection.cs b/src/JWT/Jwk/JwtWebKeysCollection.cs index 742a76592..4adf3483c 100644 --- a/src/JWT/Jwk/JwtWebKeysCollection.cs +++ b/src/JWT/Jwk/JwtWebKeysCollection.cs @@ -4,7 +4,7 @@ namespace JWT.Jwk { - public class JwtWebKeysCollection : IJwtWebKeysCollection + public sealed class JwtWebKeysCollection : IJwtWebKeysCollection { private readonly Dictionary _keys; From 0112fef814cf820d494c9c4cef52dbbde46bf12f Mon Sep 17 00:00:00 2001 From: Alexander Ignatovich Date: Fri, 3 May 2024 10:27:56 +0300 Subject: [PATCH 47/47] rename method --- src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs b/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs index bbe55ed57..ea05ab3a8 100644 --- a/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs +++ b/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs @@ -17,7 +17,7 @@ public static byte[] Base64UrlDecode(string input) for (var i = 0; i < input.Length; ++i) { - char ch = DecodeCharacter(input[i]); + char ch = Transform(input[i]); buffer[i] = ch; } @@ -44,16 +44,16 @@ private static int GetNumBase64PaddingCharsToAddForDecode(int length) } } - private static char DecodeCharacter(char ch) + private static char Transform(char symbol) { - switch (ch) + switch (symbol) { case '-': return '+'; case '_': return '/'; default: - return ch; + return symbol; } } }