From bf492c378fc2b3883404a6fd6a23ddec7f771098 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Tue, 5 Apr 2022 10:36:05 -0700 Subject: [PATCH 1/4] Redo --- README.md | 32 +++++ src/JWT/Algorithms/ECDSAAlgorithmFactory.cs | 2 +- src/JWT/Algorithms/IJwtAlgorithm.cs | 2 +- src/JWT/Algorithms/NoneAlgorithm.cs | 6 +- src/JWT/Algorithms/RS1024Algorithm.cs | 2 +- src/JWT/Algorithms/RS2048Algorithm.cs | 2 +- src/JWT/Algorithms/RS384Algorithm.cs | 2 +- src/JWT/Algorithms/RS4096Algorithm.cs | 2 +- src/JWT/Algorithms/RSAlgorithm.cs | 8 +- src/JWT/Algorithms/RSAlgorithmFactory.cs | 4 +- src/JWT/Builder/JwtBuilder.cs | 65 ++++++++-- src/JWT/JWT.csproj | 4 +- src/JWT/JwtDecoder.cs | 2 +- src/JWT/JwtValidator.cs | 58 +++++---- src/JWT/ValidationParameters.cs | 73 ++++++++++++ .../Builder/JwtBuilderDecodeTests.cs | 79 ++---------- .../JwtBuilderEndToEndTests.cs | 18 +-- tests/JWT.Tests.Common/JwtValidatorTests.cs | 112 ++++++++++++------ 18 files changed, 303 insertions(+), 170 deletions(-) create mode 100644 src/JWT/ValidationParameters.cs diff --git a/README.md b/README.md index bf326dd07..98eae2aaa 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,38 @@ var alg = header.Algorithm; // RS256 var kid = header.KeyId; // CFAEAE2D650A6CA9862575DE54371EA980643849 ``` +### Turning off parts of token validation + +If you wish to validate a token but ignore certain parts of the validation (such as the lifetime of the token when refreshing the token), you can pass a `ValidateParameters` object to the constructor of the `JwtValidator` class. + +```c# +var validationParameters = new ValidationParameters +{ + ValidateSignature = true, + ValidateExpirationTime = true, + ValidateIssuedTime = true +}; +IJwtValidator validator = new JwtValidator(serializer, provider, validationParameters); +IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); +var json = decoder.Decode(expiredToken, secret, verify: true); // will not throw because of expired token +``` + +#### Or using the fluent builder API + +```c# +var json = JwtBuilder.Create() + .WithAlgorithm(new HMACSHA256Algorithm()) + .WithSecret(secret) + .WithValidationParameters( + new ValidationParameters + { + ValidateSignature = true, + ValidateExpirationTime = true, + ValidateIssuedTime = true + }) + .Decode(expiredToken); +``` + ### Custom JSON serializer By default JSON serialization is performed by JsonNetSerializer implemented using [Json.Net](https://www.json.net). To use a different one, implement the `IJsonSerializer` interface: diff --git a/src/JWT/Algorithms/ECDSAAlgorithmFactory.cs b/src/JWT/Algorithms/ECDSAAlgorithmFactory.cs index 4b44ebe1d..ac1ad4ab6 100644 --- a/src/JWT/Algorithms/ECDSAAlgorithmFactory.cs +++ b/src/JWT/Algorithms/ECDSAAlgorithmFactory.cs @@ -126,4 +126,4 @@ private IJwtAlgorithm CreateES512Algorithm() } #endif } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/IJwtAlgorithm.cs b/src/JWT/Algorithms/IJwtAlgorithm.cs index f1c8a046d..39802a1eb 100644 --- a/src/JWT/Algorithms/IJwtAlgorithm.cs +++ b/src/JWT/Algorithms/IJwtAlgorithm.cs @@ -37,4 +37,4 @@ public static class JwtAlgorithmExtensions public static bool IsAsymmetric(this IJwtAlgorithm alg) => alg is IAsymmetricAlgorithm; } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/NoneAlgorithm.cs b/src/JWT/Algorithms/NoneAlgorithm.cs index e2cd7501f..a7b196819 100644 --- a/src/JWT/Algorithms/NoneAlgorithm.cs +++ b/src/JWT/Algorithms/NoneAlgorithm.cs @@ -7,17 +7,17 @@ namespace JWT.Algorithms /// Implements the "None" algorithm. /// /// RFC-7519 - public class NoneAlgorithm : IJwtAlgorithm + public sealed class NoneAlgorithm : IJwtAlgorithm { /// public string Name => "none"; /// - public HashAlgorithmName HashAlgorithmName => + public HashAlgorithmName HashAlgorithmName => throw new NotSupportedException("The None algorithm doesn't have any hash algorithm."); /// public byte[] Sign(byte[] key, byte[] bytesToSign) => throw new NotSupportedException("The None algorithm doesn't support signing."); } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/RS1024Algorithm.cs b/src/JWT/Algorithms/RS1024Algorithm.cs index aeea4d8e3..7fa9ff44a 100644 --- a/src/JWT/Algorithms/RS1024Algorithm.cs +++ b/src/JWT/Algorithms/RS1024Algorithm.cs @@ -45,4 +45,4 @@ public RS1024Algorithm(X509Certificate2 cert) /// public override HashAlgorithmName HashAlgorithmName => HashAlgorithmName.SHA512; } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/RS2048Algorithm.cs b/src/JWT/Algorithms/RS2048Algorithm.cs index 52a38c567..872881885 100644 --- a/src/JWT/Algorithms/RS2048Algorithm.cs +++ b/src/JWT/Algorithms/RS2048Algorithm.cs @@ -45,4 +45,4 @@ public RS2048Algorithm(X509Certificate2 cert) /// public override HashAlgorithmName HashAlgorithmName => HashAlgorithmName.SHA512; } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/RS384Algorithm.cs b/src/JWT/Algorithms/RS384Algorithm.cs index a4770a84e..9cb1a38d0 100644 --- a/src/JWT/Algorithms/RS384Algorithm.cs +++ b/src/JWT/Algorithms/RS384Algorithm.cs @@ -45,4 +45,4 @@ public RS384Algorithm(X509Certificate2 cert) /// public override HashAlgorithmName HashAlgorithmName => HashAlgorithmName.SHA384; } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/RS4096Algorithm.cs b/src/JWT/Algorithms/RS4096Algorithm.cs index 1dda6b5f2..863e76293 100644 --- a/src/JWT/Algorithms/RS4096Algorithm.cs +++ b/src/JWT/Algorithms/RS4096Algorithm.cs @@ -45,4 +45,4 @@ public RS4096Algorithm(X509Certificate2 cert) /// public override HashAlgorithmName HashAlgorithmName => HashAlgorithmName.SHA512; } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/RSAlgorithm.cs b/src/JWT/Algorithms/RSAlgorithm.cs index fc50a10aa..14c9cfcf0 100644 --- a/src/JWT/Algorithms/RSAlgorithm.cs +++ b/src/JWT/Algorithms/RSAlgorithm.cs @@ -17,7 +17,7 @@ public abstract class RSAlgorithm : IAsymmetricAlgorithm /// /// The public key for verifying the data. /// The private key for signing the data. - public RSAlgorithm(RSA publicKey, RSA privateKey) + protected RSAlgorithm(RSA publicKey, RSA privateKey) { _publicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey)); _privateKey = privateKey ?? throw new ArgumentNullException(nameof(privateKey)); @@ -30,7 +30,7 @@ public RSAlgorithm(RSA publicKey, RSA privateKey) /// An instance created using this constructor can only be used for verifying the data, not for signing it. /// /// The public key for verifying the data. - public RSAlgorithm(RSA publicKey) + protected RSAlgorithm(RSA publicKey) { _publicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey)); _privateKey = null; @@ -40,7 +40,7 @@ public RSAlgorithm(RSA publicKey) /// Creates an instance using the provided certificate. /// /// The certificate having a public key and an optional private key. - public RSAlgorithm(X509Certificate2 cert) + protected RSAlgorithm(X509Certificate2 cert) { _publicKey = GetPublicKey(cert) ?? throw new Exception("Certificate's PublicKey cannot be null."); _privateKey = GetPrivateKey(cert); @@ -105,4 +105,4 @@ private static RSA GetPublicKey(X509Certificate2 cert) #endif } } -} +} \ No newline at end of file diff --git a/src/JWT/Algorithms/RSAlgorithmFactory.cs b/src/JWT/Algorithms/RSAlgorithmFactory.cs index f7b04bfde..48bc42f4c 100644 --- a/src/JWT/Algorithms/RSAlgorithmFactory.cs +++ b/src/JWT/Algorithms/RSAlgorithmFactory.cs @@ -116,6 +116,7 @@ private RS512Algorithm CreateRS512Algorithm() throw new InvalidOperationException("Can't create a new algorithm without a certificate factory, private key or public key"); } + private RS1024Algorithm CreateRS1024Algorithm() { if (_certFactory is object) @@ -133,6 +134,7 @@ private RS1024Algorithm CreateRS1024Algorithm() throw new InvalidOperationException("Can't create a new algorithm without a certificate factory, private key or public key"); } + private RS2048Algorithm CreateRS2048Algorithm() { if (_certFactory is object) @@ -169,4 +171,4 @@ private RS4096Algorithm CreateRS4096Algorithm() throw new InvalidOperationException("Can't create a new algorithm without a certificate factory, private key or public key"); } } -} +} \ No newline at end of file diff --git a/src/JWT/Builder/JwtBuilder.cs b/src/JWT/Builder/JwtBuilder.cs index 7b15e32c2..f7ca8a130 100644 --- a/src/JWT/Builder/JwtBuilder.cs +++ b/src/JWT/Builder/JwtBuilder.cs @@ -21,11 +21,11 @@ public sealed class JwtBuilder private IJsonSerializer _serializer = new JsonNetSerializer(); private IBase64UrlEncoder _urlEncoder = new JwtBase64UrlEncoder(); private IDateTimeProvider _dateTimeProvider = new UtcDateTimeProvider(); + private ValidationParameters _valParams = ValidationParameters.Default; private IJwtAlgorithm _algorithm; private IAlgorithmFactory _algFactory; private byte[][] _secrets; - private bool _verify; /// /// Creates a new instance of instance @@ -44,7 +44,7 @@ public JwtBuilder AddHeader(HeaderName name, object value) _jwt.Header.Add(name.GetHeaderName(), value); return this; } - + /// /// Add header to the JWT. /// @@ -208,7 +208,19 @@ public JwtBuilder DoNotVerifySignature() => /// Current builder instance public JwtBuilder WithVerifySignature(bool verify) { - _verify = verify; + _valParams = _valParams.With(p => p.ValidateSignature = verify); + + return this; + } + + /// + /// Instructs whether to verify the JWT signature and what parts of the validation to perform. + /// + /// Parameters to be used for validation + /// Current builder instance + public JwtBuilder WithValidationParameters(ValidationParameters valParams) + { + _valParams = valParams; return this; } @@ -233,7 +245,7 @@ public string Decode(string token) { EnsureCanDecode(); - return _verify ? _decoder.Decode(token, _secrets, _verify) : _decoder.Decode(token); + return _decoder.Decode(token, _secrets, _valParams.ValidateSignature); } /// @@ -253,7 +265,7 @@ public string DecodeHeader(string token) /// The JWT public T DecodeHeader(string token) { - EnsureCanDecode(); + EnsureCanDecodeHeader(); return _decoder.DecodeHeader(token); } @@ -267,7 +279,7 @@ public T Decode(string token) { EnsureCanDecode(); - return _verify ? _decoder.DecodeToObject(token, _secrets, _verify) : _decoder.DecodeToObject(token); + return _decoder.DecodeToObject(token, _secrets, _valParams.ValidateSignature); } private void TryCreateEncoder() @@ -295,10 +307,20 @@ private void TryCreateDecoder() _decoder = new JwtDecoder(_serializer, _validator, _urlEncoder, _algorithm); else if (_algFactory is object) _decoder = new JwtDecoder(_serializer, _validator, _urlEncoder, _algFactory); - else if (!_verify) + else if (!_valParams.ValidateSignature) _decoder = new JwtDecoder(_serializer, _urlEncoder); } + private void TryCreateDecoderForHeader() + { + if (_serializer is null) + throw new InvalidOperationException($"Can't instantiate {nameof(JwtDecoder)}. Call {nameof(WithSerializer)}."); + if (_urlEncoder is null) + throw new InvalidOperationException($"Can't instantiate {nameof(JwtDecoder)}. Call {nameof(WithUrlEncoder)}."); + + _decoder = new JwtDecoder(_serializer, _urlEncoder); + } + private void TryCreateValidator() { if (_validator is object) @@ -309,7 +331,7 @@ private void TryCreateValidator() if (_dateTimeProvider is null) throw new InvalidOperationException($"Can't instantiate {nameof(JwtValidator)}. Call {nameof(WithDateTimeProvider)}."); - _validator = new JwtValidator(_serializer, _dateTimeProvider); + _validator = new JwtValidator(_serializer, _dateTimeProvider, _valParams); } private void EnsureCanEncode() @@ -342,6 +364,20 @@ private void EnsureCanDecode() } } + private void EnsureCanDecodeHeader() + { + if (_decoder is null) + TryCreateDecoderForHeader(); + + if (!CanDecodeHeader()) + { + throw new InvalidOperationException( + "Can't decode a token header. Check if you have call all of the following methods:" + Environment.NewLine + + $"-{nameof(WithSerializer)}" + Environment.NewLine + + $"-{nameof(WithUrlEncoder)}."); + } + } + /// /// Checks whether enough dependencies were supplied to encode a new token. /// @@ -359,10 +395,21 @@ private bool CanDecode() if (_urlEncoder is null) return false; - if (_verify) + if (_valParams.ValidateSignature) return _validator is object && (_algorithm is object || _algFactory is object); return true; } + + private bool CanDecodeHeader() + { + if (_urlEncoder is null) + return false; + + if (_serializer is null) + return false; + + return true; + } } } \ No newline at end of file diff --git a/src/JWT/JWT.csproj b/src/JWT/JWT.csproj index 75a64a859..87598b602 100644 --- a/src/JWT/JWT.csproj +++ b/src/JWT/JWT.csproj @@ -1,4 +1,4 @@ - + netstandard1.3;netstandard2.0;net5.0;net6.0;net35;net40;net46; @@ -27,7 +27,7 @@ Alexander Batishchev, John Sheehan, Michael Lehenbauer jwt;json;authorization CC0-1.0 - 9.0.0-beta1 + 9.0.0-beta2 9.0.0.0 9.0.0.0 JWT diff --git a/src/JWT/JwtDecoder.cs b/src/JWT/JwtDecoder.cs index 4dc94e174..11805b868 100644 --- a/src/JWT/JwtDecoder.cs +++ b/src/JWT/JwtDecoder.cs @@ -261,7 +261,7 @@ public void Validate(JwtParts jwt, params byte[][] keys) private static bool AllKeysHaveValues(byte[][] keys) { if (keys is null) - return true; + return false; if (keys.Length == 0) return false; diff --git a/src/JWT/JwtValidator.cs b/src/JWT/JwtValidator.cs index 570fcdb79..e8ff7282e 100644 --- a/src/JWT/JwtValidator.cs +++ b/src/JWT/JwtValidator.cs @@ -27,7 +27,7 @@ public sealed class JwtValidator : IJwtValidator private readonly IJsonSerializer _jsonSerializer; private readonly IDateTimeProvider _dateTimeProvider; private readonly IBase64UrlEncoder _urlEncoder; - private readonly int _timeMargin; + private readonly ValidationParameters _valParams; /// /// Creates an instance of @@ -35,7 +35,7 @@ public sealed class JwtValidator : IJwtValidator /// The JSON serializer /// The DateTime provider public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider) - : this(jsonSerializer, dateTimeProvider, 0) + : this(jsonSerializer, dateTimeProvider, ValidationParameters.Default) { } @@ -44,9 +44,9 @@ public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimePr /// /// The JSON serializer /// The DateTime provider - /// Time margin in seconds for exp and nbf validation - public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider, int timeMargin) - : this(jsonSerializer, dateTimeProvider, null, timeMargin) + /// Validation parameters that are passed on to + public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider, ValidationParameters valParams) + : this(jsonSerializer, dateTimeProvider, valParams, null) { } @@ -55,30 +55,16 @@ public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimePr /// /// The JSON serializer /// The DateTime provider + /// Validation parameters that are passed on to /// The base64 URL Encoder - public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider, IBase64UrlEncoder urlEncoder) - : this(jsonSerializer, dateTimeProvider, urlEncoder, 0) - { - } - - /// - /// Creates an instance of with time margin - /// - /// The JSON serializer - /// The DateTime provider - /// The base64 URL Encoder - /// Time margin in seconds for exp and nbf validation - public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider, IBase64UrlEncoder urlEncoder, int timeMargin) + public JwtValidator(IJsonSerializer jsonSerializer, IDateTimeProvider dateTimeProvider, ValidationParameters valParams, IBase64UrlEncoder urlEncoder) { _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); _dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider)); + _valParams = valParams ?? throw new ArgumentNullException(nameof(valParams)); // can be null _urlEncoder = urlEncoder; - - if (timeMargin < 0) - throw new ArgumentOutOfRangeException(nameof(timeMargin), "Value cannot be negative"); - _timeMargin = timeMargin; } /// @@ -145,7 +131,7 @@ private Exception GetValidationException(string payloadJson, string decodedCrypt if (AreAllDecodedSignaturesNullOrWhiteSpace(decodedSignatures)) return new ArgumentException(nameof(decodedSignatures)); - if (!IsAnySignatureValid(decodedCrypto, decodedSignatures)) + if (_valParams.ValidateSignature && !IsAnySignatureValid(decodedCrypto, decodedSignatures)) return new SignatureVerificationException(decodedCrypto, decodedSignatures); return GetValidationException(payloadJson); @@ -153,7 +139,7 @@ private Exception GetValidationException(string payloadJson, string decodedCrypt private Exception GetValidationException(IAsymmetricAlgorithm alg, string payloadJson, byte[] bytesToSign, byte[] decodedSignature) { - if (!alg.Verify(bytesToSign, decodedSignature)) + if (_valParams.ValidateSignature && !alg.Verify(bytesToSign, decodedSignature)) return new SignatureVerificationException("The signature is invalid according to the validation procedure."); return GetValidationException(payloadJson); @@ -169,7 +155,19 @@ private Exception GetValidationException(string payloadJson) var now = _dateTimeProvider.GetNow(); var secondsSinceEpoch = UnixEpoch.GetSecondsSince(now); - return ValidateExpClaim(payloadData, secondsSinceEpoch) ?? ValidateNbfClaim(payloadData, secondsSinceEpoch); + Exception exception = null; + + if (_valParams.ValidateExpirationTime) + { + exception = ValidateExpClaim(payloadData, secondsSinceEpoch); + } + + if (_valParams.ValidateIssuedTime) + { + exception ??= ValidateNbfClaim(payloadData, secondsSinceEpoch); + } + + return exception; } private static bool AreAllDecodedSignaturesNullOrWhiteSpace(string[] decodedSignatures) => @@ -199,7 +197,7 @@ private static bool CompareCryptoWithSignature(string decodedCrypto, string deco /// /// Verifies the 'exp' claim. /// - /// See https://tools.ietf.org/html/rfc7515#section-4.1.4 + /// See https://tools.ietf.org/html/rfc7519#section-4.1.4 /// /// private Exception ValidateExpClaim(IReadOnlyPayloadDictionary payloadData, double secondsSinceEpoch) @@ -220,7 +218,7 @@ private Exception ValidateExpClaim(IReadOnlyPayloadDictionary payloadData, doubl return new SignatureVerificationException("Claim 'exp' must be a number."); } - if (secondsSinceEpoch - _timeMargin >= expValue) + if (secondsSinceEpoch - _valParams.TimeMargin >= expValue) { return new TokenExpiredException("Token has expired.") { @@ -235,7 +233,7 @@ private Exception ValidateExpClaim(IReadOnlyPayloadDictionary payloadData, doubl /// /// Verifies the 'nbf' claim. /// - /// See https://tools.ietf.org/html/rfc7515#section-4.1.5 + /// See https://tools.ietf.org/html/rfc7519#section-4.1.5 /// private Exception ValidateNbfClaim(IReadOnlyPayloadDictionary payloadData, double secondsSinceEpoch) { @@ -255,7 +253,7 @@ private Exception ValidateNbfClaim(IReadOnlyPayloadDictionary payloadData, doubl return new SignatureVerificationException("Claim 'nbf' must be a number."); } - if (secondsSinceEpoch + _timeMargin < nbfValue) + if (secondsSinceEpoch + _valParams.TimeMargin < nbfValue) { return new SignatureVerificationException("Token is not yet valid."); } @@ -263,4 +261,4 @@ private Exception ValidateNbfClaim(IReadOnlyPayloadDictionary payloadData, doubl return null; } } -} +} \ No newline at end of file diff --git a/src/JWT/ValidationParameters.cs b/src/JWT/ValidationParameters.cs new file mode 100644 index 000000000..ec0642fb9 --- /dev/null +++ b/src/JWT/ValidationParameters.cs @@ -0,0 +1,73 @@ +using System; + +namespace JWT +{ + /// + /// Contains a set of parameters that are used by a when validating a token. + /// + public class ValidationParameters + { + /// + /// Use if you'd like to set all properties set to + /// or use if you'd like to set all properties set to . + /// > + private ValidationParameters() + { + } + + /// + /// Gets or sets whether to validate the validity of the token's signature. + /// + public bool ValidateSignature { get; set; } + + /// + /// Gets or sets whether to validate the validity of the token's expiration time. + /// + public bool ValidateExpirationTime { get; set; } + + /// + /// Gets or sets whether to validate the validity of the token's issued time. + /// + public bool ValidateIssuedTime { get; set; } + + /// + /// Gets or sets an integer to control the time margin in seconds for exp and nbf during token validation. + /// + public int TimeMargin { get; set; } + + /// + /// Returns a with all properties set to . + /// + public static ValidationParameters None => new ValidationParameters + { + ValidateSignature = false, + ValidateExpirationTime = false, + ValidateIssuedTime = false, + TimeMargin = 0 + }; + + /// + /// Returns a with all properties set to . + /// + public static ValidationParameters Default => new ValidationParameters + { + ValidateSignature = true, + ValidateExpirationTime = true, + ValidateIssuedTime = true, + TimeMargin = 0 + }; + } + + public static class ValidationParametersExtensions + { + public static ValidationParameters With(this ValidationParameters @this, Action action) + { + if (action is null) + throw new ArgumentNullException(nameof(action)); + + action(@this); + + return @this; + } + } +} \ No newline at end of file diff --git a/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs b/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs index f08a7eda3..376744870 100644 --- a/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs +++ b/tests/JWT.Tests.Common/Builder/JwtBuilderDecodeTests.cs @@ -60,10 +60,11 @@ public void DecodeHeader_To_Dictionary_Should_Return_Header() } [TestMethod] - public void Decode_Should_Return_Token() + public void Decode_Using_Symmetric_Algorithm_Should_Return_Token() { var token = JwtBuilder.Create() - .WithAlgorithm(TestData.RS256Algorithm) + .WithAlgorithm(TestData.HMACSHA256Algorithm) + .WithSecret(TestData.Secret) .Decode(TestData.Token); token.Should() @@ -71,20 +72,22 @@ public void Decode_Should_Return_Token() } [TestMethod] - public void Decode_Using_None_Algorithm_Should_Return_Token() + public void Decode_Using_Asymmetric_Algorithm_Should_Return_Token() { var token = JwtBuilder.Create() - .WithAlgorithm(new NoneAlgorithm()) - .Decode(TestData.Token); + .WithAlgorithm(TestData.RS256Algorithm) + .Decode(TestData.TokenByAsymmetricAlgorithm); token.Should() .NotBeNullOrEmpty("because the decoded token contains values and they should have been decoded"); } [TestMethod] - public void Decode_Using_WithAlgorithm_Should_Return_Token() + public void Decode_Using_None_Algorithm_Should_Return_Token() { var token = JwtBuilder.Create() + .WithAlgorithm(new NoneAlgorithm()) + .DoNotVerifySignature() .Decode(TestData.Token); token.Should() @@ -167,20 +170,6 @@ public void Decode_Without_Serializer_Should_Throw_Exception() .Throw("because token can't be decoded without valid serializer"); } - [TestMethod] - public void Decode_With_Serializer_Should_Return_Token() - { - var serializer = new JsonNetSerializer(); - - var token = JwtBuilder.Create() - .WithAlgorithm(TestData.RS256Algorithm) - .WithSerializer(serializer) - .Decode(TestData.Token); - - token.Should() - .NotBeNullOrEmpty("because token should be correctly decoded and its data extracted"); - } - [TestMethod] public void Decode_Without_UrlEncoder_Should_Throw_Exception() { @@ -194,20 +183,6 @@ public void Decode_Without_UrlEncoder_Should_Throw_Exception() .Throw("because token can't be decoded without valid UrlEncoder"); } - [TestMethod] - public void Decode_With_UrlEncoder_Should_Return_Token() - { - var urlEncoder = new JwtBase64UrlEncoder(); - - var token = JwtBuilder.Create() - .WithAlgorithm(TestData.RS256Algorithm) - .WithUrlEncoder(urlEncoder) - .Decode(TestData.Token); - - token.Should() - .NotBeNullOrEmpty("because token should have been correctly decoded with the valid base64 encoder"); - } - [TestMethod] public void Decode_Without_TimeProvider_Should_Throw_Exception() { @@ -221,47 +196,17 @@ public void Decode_Without_TimeProvider_Should_Throw_Exception() .Throw("because token can't be decoded without valid DateTimeProvider"); } - [TestMethod] - public void Decode_With_DateTimeProvider_Should_Return_Token() - { - var dateTimeProvider = new UtcDateTimeProvider(); - - - var token = JwtBuilder.Create() - .WithDateTimeProvider(dateTimeProvider) - .WithAlgorithm(TestData.RS256Algorithm) - .Decode(TestData.Token); - - token.Should() - .NotBeNullOrEmpty("because the decoding process must be successful with valid DateTimeProvider"); - } - [TestMethod] public void Decode_Without_Validator_Should_Return_Token() { var token = JwtBuilder.Create() .WithAlgorithm(TestData.RS256Algorithm) .WithValidator(null) + .DoNotVerifySignature() .Decode(TestData.Token); token.Should() - .NotBeNullOrEmpty("because a JWT should not necessary have validator to be decoded"); - } - - [TestMethod] - public void Decode_With_ExplicitValidator_Should_Return_Token() - { - var validator = new JwtValidator( - new JsonNetSerializer(), - new UtcDateTimeProvider()); - - var token = JwtBuilder.Create() - .WithAlgorithm(TestData.RS256Algorithm) - .WithValidator(validator) - .Decode(TestData.Token); - - token.Should() - .NotBeNullOrEmpty("because a JWT should be correctly decoded, even with validator"); + .NotBeNullOrEmpty("because a JWT should not necessary have validator to be decoded"); } [TestMethod] @@ -274,7 +219,7 @@ public void Decode_With_VerifySignature_Should_Return_Token_When_Algorithm_Is_Sy .Decode(TestData.Token); token.Should() - .NotBeNullOrEmpty("because the signature must have been verified successfully and the JWT correctly decoded"); + .NotBeNullOrEmpty("because the signature must have been verified successfully and the JWT correctly decoded"); } [TestMethod] diff --git a/tests/JWT.Tests.Common/JwtBuilderEndToEndTests.cs b/tests/JWT.Tests.Common/JwtBuilderEndToEndTests.cs index f8986ed09..c1ec0d00e 100644 --- a/tests/JWT.Tests.Common/JwtBuilderEndToEndTests.cs +++ b/tests/JWT.Tests.Common/JwtBuilderEndToEndTests.cs @@ -40,6 +40,15 @@ public void Encode_and_Decode_With_Certificate() .Should() .HaveCount(3, "because the token should consist of three parts"); + var jwt = builder.WithAlgorithm(algorithm) + .MustVerifySignature() + .Decode>(token); + + jwt["iss"].Should().Be(iss); + jwt["exp"].Should().Be(exp); + jwt[nameof(Customer.FirstName)].Should().Be(TestData.Customer.FirstName); + jwt[nameof(Customer.Age)].Should().Be(TestData.Customer.Age); + var header = builder.DecodeHeader(token); header.Type @@ -51,15 +60,6 @@ public void Encode_and_Decode_With_Certificate() header.KeyId .Should() .Be(TestData.ServerRsaPublicThumbprint1); - - var jwt = builder.WithAlgorithm(algorithm) - .MustVerifySignature() - .Decode>(token); - - jwt["iss"].Should().Be(iss); - jwt["exp"].Should().Be(exp); - jwt[nameof(Customer.FirstName)].Should().Be(TestData.Customer.FirstName); - jwt[nameof(Customer.Age)].Should().Be(TestData.Customer.Age); } private static X509Certificate2 CreateCertificate() diff --git a/tests/JWT.Tests.Common/JwtValidatorTests.cs b/tests/JWT.Tests.Common/JwtValidatorTests.cs index 23154d7fd..5508ab1e4 100644 --- a/tests/JWT.Tests.Common/JwtValidatorTests.cs +++ b/tests/JWT.Tests.Common/JwtValidatorTests.cs @@ -3,10 +3,9 @@ using JWT.Algorithms; using JWT.Exceptions; using JWT.Serializers; -using JWT.Tests.Stubs; using JWT.Tests.Models; +using JWT.Tests.Stubs; using Microsoft.VisualStudio.TestTools.UnitTesting; - using static JWT.Internal.EncodingHelper; namespace JWT.Tests @@ -14,6 +13,41 @@ namespace JWT.Tests [TestClass] public class JwtValidatorTests { + [TestMethod] + public void Ctor_Should_Throw_Exception_When_Serializer_Is_Null() + { + var dateTimeProvider = new UtcDateTimeProvider(); + + Action action = () => new JwtValidator(null, dateTimeProvider); + + action.Should() + .Throw("because the serializer must not be null"); + } + + [TestMethod] + public void Ctor_Should_Throw_Exception_When_DateTimeProvider_Is_Null() + { + var serializer = new JsonNetSerializer(); + + Action action = () => new JwtValidator(serializer, null); + + action.Should() + .Throw("because the DateTime provider must not be null"); + } + + [TestMethod] + public void Ctor_Should_Throw_Exception_When_ValidationParameters_Are_Null() + { + var serializer = new JsonNetSerializer(); + var dateTimeProvider = new UtcDateTimeProvider(); + + Action action = () => new JwtValidator(serializer, dateTimeProvider, null); + + action.Should() + .Throw("because the validation parameters must not be null"); + } + + [DataTestMethod] [DataRow(null, null, null)] [DataRow("", null, null)] @@ -22,9 +56,9 @@ public class JwtValidatorTests [DataRow("{}", TestData.Token, "")] public void Validate_Should_Throw_Exception_When_Argument_Is_Null_Or_Empty(string payloadJson, string decodedCrypto, string decodedSignature) { - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new UtcDateTimeProvider(); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new UtcDateTimeProvider(); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); Action action = () => jwtValidator.Validate(payloadJson, decodedCrypto, decodedSignature); @@ -37,8 +71,8 @@ public void Validate_Should_Throw_Exception_When_Signature_Is_Invalid() { const string token = TestData.Token; var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new UtcDateTimeProvider(); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new UtcDateTimeProvider(); var jwt = new JwtParts(token); var payloadJson = GetString(urlEncoder.Decode(jwt.Payload)); @@ -52,7 +86,7 @@ public void Validate_Should_Throw_Exception_When_Signature_Is_Invalid() ++signatureData[0]; // malformed signature var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); Action action = () => jwtValidator.Validate(payloadJson, decodedCrypto, decodedSignature); @@ -64,8 +98,8 @@ public void Validate_Should_Throw_Exception_When_Signature_Is_Invalid() public void Validate_Should_Not_Throw_Exception_When_Crypto_Matches_Signature() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new UtcDateTimeProvider(); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new UtcDateTimeProvider(); var jwt = new JwtParts(TestData.Token); @@ -79,7 +113,7 @@ public void Validate_Should_Not_Throw_Exception_When_Crypto_Matches_Signature() var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); jwtValidator.Validate(payloadJson, decodedCrypto, decodedSignature); } @@ -91,9 +125,9 @@ public void Validate_Should_Not_Throw_Exception_When_Crypto_Matches_Signature() [DataRow("{}", TestData.Token, "")] public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Argument_Is_Null_Or_Empty(string payloadJson, string decodedCrypto, string decodedSignature) { - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new UtcDateTimeProvider(); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new UtcDateTimeProvider(); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); @@ -108,8 +142,8 @@ public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Argument public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Signature_Is_Not_Valid() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new UtcDateTimeProvider(); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new UtcDateTimeProvider(); var jwt = new JwtParts(TestData.Token); @@ -124,7 +158,7 @@ public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Signatur ++signatureData[0]; // malformed signature var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); isValid.Should() @@ -138,8 +172,8 @@ public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Signatur public void TryValidate_Should_Return_True_And_Exception_Null_When_Crypto_Signature_Is_Valid() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new UtcDateTimeProvider(); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new UtcDateTimeProvider(); var jwt = new JwtParts(TestData.Token); @@ -153,7 +187,7 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Crypto_Signat var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); isValid.Should() @@ -167,8 +201,8 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Crypto_Signat public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Token_Is_Expired() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp)); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp)); var jwt = new JwtParts(TestData.TokenWithExp); @@ -182,7 +216,7 @@ public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Token_Is var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); isValid.Should() @@ -199,8 +233,8 @@ public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Token_Is public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Not_Expired() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp - 1)); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp - 1)); var jwt = new JwtParts(TestData.TokenWithExp); @@ -214,7 +248,7 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Not_ var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); isValid.Should() @@ -228,8 +262,9 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Not_ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Expired_But_Validator_Has_Time_Margin() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp)); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp)); + var valParams = ValidationParameters.Default.With(p => p.TimeMargin = 1); var jwt = new JwtParts(TestData.TokenWithExp); @@ -243,7 +278,7 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Expi var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider, timeMargin: 1); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider, valParams); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); ex.Should() @@ -257,8 +292,8 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Expi public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Token_Is_Not_Yet_Usable() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp - 1)); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp - 1)); var jwt = new JwtParts(TestData.TokenWithNbf); @@ -272,7 +307,7 @@ public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Token_Is var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); isValid.Should() @@ -287,8 +322,8 @@ public void TryValidate_Should_Return_False_And_Exception_Not_Null_When_Token_Is public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Usable() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp)); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp)); var jwt = new JwtParts(TestData.TokenWithNbf); @@ -302,7 +337,7 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Usab var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); isValid.Should() @@ -316,8 +351,9 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Usab public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Not_Yet_Usable_But_Validator_Has_Time_Margin() { var urlEncoder = new JwtBase64UrlEncoder(); - var jsonNetSerializer = new JsonNetSerializer(); - var utcDateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp - 1)); + var jsonSerializer = new JsonNetSerializer(); + var dateTimeProvider = new StaticDateTimeProvider(DateTimeOffset.FromUnixTimeSeconds(TestData.TokenTimestamp - 1)); + var valParams = ValidationParameters.Default.With(p => p.TimeMargin = 1); var jwt = new JwtParts(TestData.TokenWithNbf); @@ -331,7 +367,7 @@ public void TryValidate_Should_Return_True_And_Exception_Null_When_Token_Is_Not_ var signatureData = alg.Sign(GetBytes(TestData.Secret), bytesToSign); var decodedSignature = Convert.ToBase64String(signatureData); - var jwtValidator = new JwtValidator(jsonNetSerializer, utcDateTimeProvider, timeMargin: 1); + var jwtValidator = new JwtValidator(jsonSerializer, dateTimeProvider, valParams); var isValid = jwtValidator.TryValidate(payloadJson, decodedCrypto, decodedSignature, out var ex); ex.Should() From 256251376bb36c47fcc28bec691fc650964fe4ac Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Tue, 5 Apr 2022 12:05:37 -0700 Subject: [PATCH 2/4] Update JWT.csproj --- src/JWT/JWT.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JWT/JWT.csproj b/src/JWT/JWT.csproj index 87598b602..f82554632 100644 --- a/src/JWT/JWT.csproj +++ b/src/JWT/JWT.csproj @@ -1,4 +1,4 @@ - + netstandard1.3;netstandard2.0;net5.0;net6.0;net35;net40;net46; @@ -56,4 +56,4 @@ - \ No newline at end of file + From 193a8a86b0e76cc9e202b276a48ceb88425d33d8 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Fri, 8 Apr 2022 11:14:17 -0700 Subject: [PATCH 3/4] Update ValidationParameters.cs --- src/JWT/ValidationParameters.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JWT/ValidationParameters.cs b/src/JWT/ValidationParameters.cs index ec0642fb9..c507ad9b4 100644 --- a/src/JWT/ValidationParameters.cs +++ b/src/JWT/ValidationParameters.cs @@ -5,7 +5,7 @@ namespace JWT /// /// Contains a set of parameters that are used by a when validating a token. /// - public class ValidationParameters + public struct ValidationParameters { /// /// Use if you'd like to set all properties set to @@ -70,4 +70,4 @@ public static ValidationParameters With(this ValidationParameters @this, Action< return @this; } } -} \ No newline at end of file +} From 0bd0b6616eb8c58b0362de1767ed4495474dfde3 Mon Sep 17 00:00:00 2001 From: Alexander Batishchev Date: Fri, 8 Apr 2022 11:23:44 -0700 Subject: [PATCH 4/4] Revert "Update ValidationParameters.cs" This reverts commit 193a8a86b0e76cc9e202b276a48ceb88425d33d8. --- src/JWT/ValidationParameters.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JWT/ValidationParameters.cs b/src/JWT/ValidationParameters.cs index c507ad9b4..ec0642fb9 100644 --- a/src/JWT/ValidationParameters.cs +++ b/src/JWT/ValidationParameters.cs @@ -5,7 +5,7 @@ namespace JWT /// /// Contains a set of parameters that are used by a when validating a token. /// - public struct ValidationParameters + public class ValidationParameters { /// /// Use if you'd like to set all properties set to @@ -70,4 +70,4 @@ public static ValidationParameters With(this ValidationParameters @this, Action< return @this; } } -} +} \ No newline at end of file