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();