diff --git a/CHANGELOG.md b/CHANGELOG.md index e2dfa5ba9..7cb3f80cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +# JWT 11.0.0-beta3 + +- Added support ofr JSON web keys + # JWT.Extensions.AspNetCore 11.0.0-beta3 - Converted to use the event model to allow dependency injection with custom event classes. @@ -47,4 +51,4 @@ - Renamed default IdentityFactory in Jwt.Extensions.AspNetCore, opened up for inheritance, extension (#428) - Added Encode(T) and Encode(Type, object) to JwtBuilder (#415) - Updated Newtonsoft.Json to version 13.0.1 -- Fixed typos in exception messages +- Fixed typos in exception messages \ No newline at end of file diff --git a/src/JWT/Algorithms/HMACSHA256Algorithm.cs b/src/JWT/Algorithms/HMACSHA256Algorithm.cs index 072058d8b..2a6d049fa 100644 --- a/src/JWT/Algorithms/HMACSHA256Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA256Algorithm.cs @@ -7,6 +7,14 @@ 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..5c36405bf 100644 --- a/src/JWT/Algorithms/HMACSHA384Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA384Algorithm.cs @@ -7,6 +7,14 @@ 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..b944f1f91 100644 --- a/src/JWT/Algorithms/HMACSHA512Algorithm.cs +++ b/src/JWT/Algorithms/HMACSHA512Algorithm.cs @@ -6,7 +6,15 @@ 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 6b53da268..54d94fc2b 100644 --- a/src/JWT/Algorithms/HMACSHAAlgorithm.cs +++ b/src/JWT/Algorithms/HMACSHAAlgorithm.cs @@ -2,8 +2,14 @@ namespace JWT.Algorithms { - public abstract class HMACSHAAlgorithm : IJwtAlgorithm + public abstract class HMACSHAAlgorithm : ISymmetricAlgorithm { + protected HMACSHAAlgorithm() + { + } + + protected HMACSHAAlgorithm(byte[] key) => this.Key = key; + /// public abstract string Name { get; } @@ -13,10 +19,12 @@ public abstract class HMACSHAAlgorithm : IJwtAlgorithm /// 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..29783ef41 100644 --- a/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs +++ b/src/JWT/Algorithms/HMACSHAAlgorithmFactory.cs @@ -5,16 +5,24 @@ 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: diff --git a/src/JWT/Algorithms/ISymmetricAlgorithm.cs b/src/JWT/Algorithms/ISymmetricAlgorithm.cs new file mode 100644 index 000000000..23bd737a8 --- /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 + { + byte[] Key { get; } + } +} \ No newline at end of file diff --git a/src/JWT/Builder/JwtBuilder.cs b/src/JWT/Builder/JwtBuilder.cs index 3f7957c42..60c8f8669 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; @@ -34,6 +35,8 @@ public sealed class JwtBuilder private IAlgorithmFactory _algFactory; private byte[][] _secrets; + private JwtWebKeysCollection _webKeysCollection; + /// /// Creates a new instance of instance /// @@ -170,6 +173,96 @@ public JwtBuilder WithAlgorithmFactory(IAlgorithmFactory algFactory) return this; } + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(JwtWebKeysCollection webKeysCollection) + { + _webKeysCollection = webKeysCollection; + _algFactory = new JwtJsonWebKeySetAlgorithmFactory(webKeysCollection); + return this; + } + + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(Func getJsonWebKeys) + { + return WithJsonWebKeySet(getJsonWebKeys()); + } + + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(IJwtWebKeysCollectionFactory webKeysCollectionFactory) + { + return WithJsonWebKeySet(webKeysCollectionFactory.CreateKeys()); + } + + /// + /// Sets Json Web Key Set + /// + /// + /// Current builder instance. + public JwtBuilder WithJsonWebKeySet(string keySet) + { + 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); + } + /// /// Sets JWT algorithm. /// diff --git a/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs b/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs new file mode 100644 index 000000000..98cc0fe8a --- /dev/null +++ b/src/JWT/Exceptions/InvalidJsonWebKeyEllipticCurveTypeException.cs @@ -0,0 +1,12 @@ +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/Exceptions/InvalidJsonWebKeyTypeException.cs b/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs new file mode 100644 index 000000000..aa8fdfe4b --- /dev/null +++ b/src/JWT/Exceptions/InvalidJsonWebKeyTypeException.cs @@ -0,0 +1,12 @@ +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/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/JWT.csproj b/src/JWT/JWT.csproj index b0fdd8fff..bcad9eaa9 100644 --- a/src/JWT/JWT.csproj +++ b/src/JWT/JWT.csproj @@ -28,7 +28,7 @@ - 11.0.0-beta2 + 11.0.0-beta3 11.0.0.0 11.0.0.0 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/IJwtWebKeysCollectionFactory.cs b/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs new file mode 100644 index 000000000..33f8f81b5 --- /dev/null +++ b/src/JWT/Jwk/IJwtWebKeysCollectionFactory.cs @@ -0,0 +1,7 @@ +namespace JWT.Jwk +{ + public interface IJwtWebKeysCollectionFactory + { + JwtWebKeysCollection CreateKeys(); + } +} \ 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..f25d26292 --- /dev/null +++ b/src/JWT/Jwk/JwtJsonWebKeyAlgorithmFactory.cs @@ -0,0 +1,124 @@ +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 publicKey = CreateRSAKey(false); + + var privateKey = CreateRSAKey(true); + + var algorithmFactory = privateKey == null ? new RSAlgorithmFactory(publicKey) : new RSAlgorithmFactory(publicKey, privateKey); + + return algorithmFactory.Create(context); + } + + private IJwtAlgorithm CreateECDSAAlgorithm(JwtDecoderContext context) + { +#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) + }, + D = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.D) + }; + + var key = ECDsa.Create(parameters); + + 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(); +#endif + + return algorithmFactory.Create(context); + } + + private IJwtAlgorithm CreateHMACSHAAlgorithm(JwtDecoderContext context) + { + var key = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.SymmetricKey); + + var algorithmFactory = new HMACSHAAlgorithmFactory(key); + + return algorithmFactory.Create(context); + } + + private RSA CreateRSAKey(bool privateKey) + { + var firstPrimeFactor = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.FirstPrimeFactor); + + if (privateKey && firstPrimeFactor == null) + return null; + + var rsaParameters = new RSAParameters + { + Modulus = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Modulus), + Exponent = JwtWebKeyPropertyValuesEncoder.Base64UrlDecode(_key.Exponent), + 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(); + + key.ImportParameters(rsaParameters); + + return key; + } + +#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 diff --git a/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs new file mode 100644 index 000000000..521eb9e70 --- /dev/null +++ b/src/JWT/Jwk/JwtJsonWebKeySetAlgorithmFactory.cs @@ -0,0 +1,42 @@ +using System; +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"); + + var algorithmFactory = new JwtJsonWebKeyAlgorithmFactory(key); + + return algorithmFactory.Create(context); + } + } +} \ No newline at end of file diff --git a/src/JWT/Jwk/JwtWebKey.cs b/src/JWT/Jwk/JwtWebKey.cs new file mode 100644 index 000000000..c5ad68cfa --- /dev/null +++ b/src/JWT/Jwk/JwtWebKey.cs @@ -0,0 +1,157 @@ +using Newtonsoft.Json; + +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER +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 NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [System.Text.Json.Serialization.JsonConstructor] + 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 NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("kty")] +#endif + public string KeyType { get; set; } + + /// + /// The "kid" parameter which defines key id + /// + [JsonProperty("kid")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [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 NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [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 NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("e")] +#endif + public string Exponent { get; set; } + + /// + /// The "p" parameter which represents a First Prime Factor for RSA algorithms + /// + [JsonProperty("p")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("p")] +#endif + public string FirstPrimeFactor { get; set; } + + /// + /// The "q" parameter which represents a Second Prime Factor exponent for RSA algorithms + /// + [JsonProperty("q")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("q")] +#endif + public string SecondPrimeFactor { get; set; } + + /// + /// The "dp" parameter which represents a First Factor CRT Exponent for RSA algorithms + /// + [JsonProperty("dp")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("dp")] +#endif + public string FirstFactorCRTExponent { get; set; } + + /// + /// The "dq" parameter which represents a Second Factor CRT Exponent for RSA algorithms + /// + [JsonProperty("dq")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("dq")] +#endif + public string SecondFactorCRTExponent { get; set; } + + /// + /// The "qi" parameter which represents a First CRT Coefficient for RSA algorithms + /// + [JsonProperty("qi")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [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" + /// + [JsonProperty("crv")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [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 NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [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 NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("y")] +#endif + public string EllipticCurveY { get; set; } + + /// + /// 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 NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("d")] +#endif + 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 + /// encoding of the octet sequence containing the key value. ("kty") must be "oct" + /// + [JsonProperty("k")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("k")] +#endif + public string SymmetricKey { get; set; } + } +} \ 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..ea05ab3a8 --- /dev/null +++ b/src/JWT/Jwk/JwtWebKeyPropertyValuesEncoder.cs @@ -0,0 +1,60 @@ +using System; + +namespace JWT.Jwk +{ + /// + /// Based on Microsoft.AspNetCore.WebUtilities.WebEncoders + /// + internal static class JwtWebKeyPropertyValuesEncoder + { + public static byte[] Base64UrlDecode(string input) + { + 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) + { + char ch = Transform(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: + throw new ArgumentOutOfRangeException (nameof(length), $"Malformed input: {length} is an invalid input length."); + } + } + + private static char Transform(char symbol) + { + switch (symbol) + { + case '-': + return '+'; + case '_': + return '/'; + default: + return symbol; + } + } + } +} diff --git a/src/JWT/Jwk/JwtWebKeySet.cs b/src/JWT/Jwk/JwtWebKeySet.cs new file mode 100644 index 000000000..23ce3bdfb --- /dev/null +++ b/src/JWT/Jwk/JwtWebKeySet.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER +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 + { + [JsonProperty("keys")] +#if NET462_OR_GREATER || NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER + [JsonPropertyName("keys")] +#endif + public IEnumerable Keys { get; set; } = null!; + } +} diff --git a/src/JWT/Jwk/JwtWebKeysCollection.cs b/src/JWT/Jwk/JwtWebKeysCollection.cs new file mode 100644 index 000000000..4adf3483c --- /dev/null +++ b/src/JWT/Jwk/JwtWebKeysCollection.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using JWT.Serializers; + +namespace JWT.Jwk +{ + public sealed 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) => _keys.TryGetValue(keyId, out var key) ? key : null; + } +} \ No newline at end of file diff --git a/src/JWT/JwtDecoder.cs b/src/JWT/JwtDecoder.cs index 7c19741de..eaf0161f5 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; @@ -240,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 - { - ValidSymmetricAlgorithm(keys, decodedPayload, algorithm, 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)); } } @@ -264,44 +266,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/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/src/JWT/JwtValidator.cs b/src/JWT/JwtValidator.cs index e6d846fe2..aa57b33b6 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,29 @@ 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) + throw new ArgumentNullException(nameof(keys)); + if (alg.Key == null && !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 is not null ? + keys.Select(key => Convert.ToBase64String(alg.Sign(key, bytesToSign))).ToArray() : + [Convert.ToBase64String(alg.Sign(null, bytesToSign))]; + + Validate(decodedPayload, rawSignature, recreatedSignatures); + } + /// /// public bool TryValidate(string payloadJson, string signature, string decodedSignature, out Exception ex) @@ -264,5 +288,27 @@ 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; + } } } diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderEncodeTests.cs index 9a6d16992..bbaa542b8 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; @@ -414,6 +412,62 @@ 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) + .Encode(TestData.Customer); + + token.Should().NotBeNullOrEmpty(); + + var decoded = JwtBuilder.Create() + .WithJsonWebKeySet(TestData.JsonWebKeySet) + .Decode(token); + + decoded.Should() + .BeEquivalentTo(TestData.Customer); + } + +#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 + + [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/Jwk/JwtWebKeysCollectionTests.cs b/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs new file mode 100644 index 000000000..0a2b5b067 --- /dev/null +++ b/tests/JWT.Tests.Common/Jwk/JwtWebKeysCollectionTests.cs @@ -0,0 +1,23 @@ +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/JwtDecoderTests.cs b/tests/JWT.Tests.Common/JwtDecoderTests.cs index bcd134e46..46a6cc09d 100644 --- a/tests/JWT.Tests.Common/JwtDecoderTests.cs +++ b/tests/JWT.Tests.Common/JwtDecoderTests.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; using AutoFixture; using FluentAssertions; using JWT.Algorithms; using JWT.Builder; using JWT.Exceptions; +using JWT.Jwk; using JWT.Serializers; using JWT.Tests.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -487,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); @@ -531,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()); @@ -543,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); @@ -558,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); @@ -571,7 +572,110 @@ 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"); } - + + [TestMethod] + 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); + + customer.Should() + .BeEquivalentTo(TestData.Customer); + } + +#if NETSTANDARD2_0 || NET6_0_OR_GREATER + [TestMethod] + 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(); + + 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); + + customer.Should() + .BeEquivalentTo(TestData.Customer); + } +#endif + + [TestMethod] + public void Should_Decode_With_Json_Web_Keys_() + { + var token = JwtBuilder.Create() + .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); + + 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"); + } + + [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); + + var token = JwtBuilder.Create() + .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"); + } + private static IJsonSerializer CreateSerializer() => new DefaultJsonSerializerFactory().Create(); } diff --git a/tests/JWT.Tests.Common/Models/TestData.cs b/tests/JWT.Tests.Common/Models/TestData.cs index 3db70255c..5d2e19731 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,6 +68,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\",\"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 { { nameof(Customer.FirstName), Customer.FirstName }, @@ -95,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();