diff --git a/src/Paseto/Validators/BaseValidator.cs b/src/Paseto/Validators/BaseValidator.cs index 8c48ccd..4aec35d 100644 --- a/src/Paseto/Validators/BaseValidator.cs +++ b/src/Paseto/Validators/BaseValidator.cs @@ -1,11 +1,12 @@ namespace Paseto.Validators; using System; +using System.Text.Json; /// /// The Base Validator. /// -/// +/// public abstract class BaseValidator : IPasetoPayloadValidator { /// @@ -38,4 +39,12 @@ public abstract class BaseValidator : IPasetoPayloadValidator /// The optional expected value. /// true if the specified value is valid; otherwise, false. public abstract bool IsValid(IComparable expected = null); + + internal static object GetValueFromJsonElement(JsonElement element) => element.ValueKind switch + { + JsonValueKind.Number => element.GetDouble(), + JsonValueKind.String => element.GetString(), + JsonValueKind.True or JsonValueKind.False => element.GetBoolean(), + _ => element.GetRawText().Trim('"') + }; } diff --git a/src/Paseto/Validators/DateValidator.cs b/src/Paseto/Validators/DateValidator.cs new file mode 100644 index 0000000..39bd8fd --- /dev/null +++ b/src/Paseto/Validators/DateValidator.cs @@ -0,0 +1,76 @@ +namespace Paseto.Validators; + +using System; +using System.Text.Json; +using Paseto.Validators.Internal; + +/// +/// The Base Date Validator. +/// +/// +public abstract class DateValidator : BaseValidator +{ + /// + /// Initializes a new instance of the class. + /// + /// The payload. + public DateValidator(PasetoPayload payload) : base(payload) { } + + /// + /// Validates the input value against the provided optional expected value. Throws an exception if not valid. + /// + /// The input value to validate. + /// The optional expected value. + public abstract void ValidateDate(IComparable value, IComparable expected = null); + + /// + /// Validates the payload against the provided optional expected value. Throws an exception if not valid. + /// + /// The optional expected value. + /// + /// Token has expired. + /// + public override void Validate(IComparable expected = null) + { + if (!Payload.TryGetValue(ClaimName, out var value)) + throw new PasetoTokenValidationException($"Claim '{ClaimName}' not found"); + + DateTime exp; + try + { + if (value is JsonElement json) + value = GetValueFromJsonElement(json); + + if (value is string s) + exp = DateTimeOffset.Parse(s).UtcDateTime; + else + exp = Convert.ToDateTime(value); + } + catch (Exception) + { + throw new PasetoTokenValidationException($"Claim '{ClaimName}' must be a DateTime"); + } + + expected ??= DateTime.UtcNow; + + ValidateDate(exp, expected); + } + + /// + /// Validates the payload against the provided optional expected value. + /// + /// The optional expected value. + /// true if the specified value is valid; otherwise, false. + public override bool IsValid(IComparable expected = null) + { + try + { + Validate(expected); + return true; + } + catch (Exception) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Paseto/Validators/EqualValidator.cs b/src/Paseto/Validators/EqualValidator.cs index daec964..80c5dbb 100644 --- a/src/Paseto/Validators/EqualValidator.cs +++ b/src/Paseto/Validators/EqualValidator.cs @@ -79,12 +79,4 @@ public override bool IsValid(IComparable expected = null) return false; } } - - private static object GetValueFromJsonElement(JsonElement element) => element.ValueKind switch - { - JsonValueKind.Number => element.GetDouble(), - JsonValueKind.String => element.GetString(), - JsonValueKind.True or JsonValueKind.False => element.GetBoolean(), - _ => element.GetRawText().Trim('"') - }; } diff --git a/src/Paseto/Validators/ExpirationTimeValidator.cs b/src/Paseto/Validators/ExpirationTimeValidator.cs index 2d6f142..462e916 100644 --- a/src/Paseto/Validators/ExpirationTimeValidator.cs +++ b/src/Paseto/Validators/ExpirationTimeValidator.cs @@ -6,8 +6,8 @@ /// /// The ExpirationTime Validator. This class cannot be inherited. /// -/// -public sealed class ExpirationTimeValidator : BaseValidator +/// +public sealed class ExpirationTimeValidator : DateValidator { /// /// Initializes a new instance of the class. @@ -21,49 +21,10 @@ public ExpirationTimeValidator(PasetoPayload payload) : base(payload) { } /// The name of the claim. public override string ClaimName => PasetoRegisteredClaimNames.ExpirationTime; - /// - /// Validates the payload against the provided optional expected value. Throws an exception if not valid. - /// - /// The optional expected value. - /// - /// Token has expired. - /// - public override void Validate(IComparable expected = null) + /// + public override void ValidateDate(IComparable value, IComparable expected = null) { - if (!Payload.TryGetValue(ClaimName, out var value)) - throw new PasetoTokenValidationException($"Claim '{ClaimName}' not found"); - - DateTime exp; - try - { - exp = Convert.ToDateTime(value); - } - catch (Exception) - { - throw new PasetoTokenValidationException($"Claim '{ClaimName}' must be a DateTime"); - } - - expected ??= DateTime.UtcNow; - - if (Comparer.GetComparisonResult(exp, expected) < 0) // expected >= exp + if (Comparer.GetComparisonResult(value, expected) < 0) // expected >= exp throw new PasetoTokenValidationException("Token has expired"); } - - /// - /// Validates the payload against the provided optional expected value. - /// - /// The optional expected value. - /// true if the specified value is valid; otherwise, false. - public override bool IsValid(IComparable expected = null) - { - try - { - Validate(expected); - return true; - } - catch (Exception) - { - return false; - } - } } diff --git a/src/Paseto/Validators/IssuedAtValidator.cs b/src/Paseto/Validators/IssuedAtValidator.cs index 4d30c6c..1f8c61d 100644 --- a/src/Paseto/Validators/IssuedAtValidator.cs +++ b/src/Paseto/Validators/IssuedAtValidator.cs @@ -6,8 +6,8 @@ /// /// The NotAfter Validator. This class cannot be inherited. /// -/// -public sealed class IssuedAtValidator : BaseValidator +/// +public sealed class IssuedAtValidator : DateValidator { /// /// Initializes a new instance of the class. @@ -21,49 +21,10 @@ public IssuedAtValidator(PasetoPayload payload) : base(payload) { } /// The name of the claim. public override string ClaimName => PasetoRegisteredClaimNames.IssuedAt; - /// - /// Validates the payload against the provided optional expected value. Throws an exception if not valid. - /// - /// The optional expected value. - /// - /// Token is not yet valid. - /// - public override void Validate(IComparable expected = null) + /// + public override void ValidateDate(IComparable value, IComparable expected = null) { - if (!Payload.TryGetValue(ClaimName, out var value)) - throw new PasetoTokenValidationException($"Claim '{ClaimName}' not found"); - - DateTime iat; - try - { - iat = Convert.ToDateTime(value); - } - catch (Exception) - { - throw new PasetoTokenValidationException($"Claim '{ClaimName}' must be a DateTime"); - } - - expected ??= DateTime.UtcNow; - - if (Comparer.GetComparisonResult(iat, expected) > 0) // expected >= iat + if (Comparer.GetComparisonResult(value, expected) > 0) // expected >= iat throw new PasetoTokenValidationException("Token is not yet valid"); } - - /// - /// Validates the payload against the provided optional expected value. - /// - /// The optional expected value. - /// true if the specified value is valid; otherwise, false. - public override bool IsValid(IComparable expected = null) - { - try - { - Validate(expected); - return true; - } - catch (Exception) - { - return false; - } - } } \ No newline at end of file diff --git a/src/Paseto/Validators/NotBeforeValidator.cs b/src/Paseto/Validators/NotBeforeValidator.cs index fd8caac..007cfe1 100644 --- a/src/Paseto/Validators/NotBeforeValidator.cs +++ b/src/Paseto/Validators/NotBeforeValidator.cs @@ -6,8 +6,8 @@ /// /// The NotBefore Validator. This class cannot be inherited. /// -/// -public sealed class NotBeforeValidator : BaseValidator +/// +public sealed class NotBeforeValidator : DateValidator { /// /// Initializes a new instance of the class. @@ -21,49 +21,10 @@ public NotBeforeValidator(PasetoPayload payload) : base(payload) { } /// The name of the claim. public override string ClaimName => PasetoRegisteredClaimNames.NotBefore; - /// - /// Validates the payload against the provided optional expected value. Throws an exception if not valid. - /// - /// The optional expected value. - /// - /// Token is not yet valid. - /// - public override void Validate(IComparable expected = null) + /// + public override void ValidateDate(IComparable value, IComparable expected = null) { - if (!Payload.TryGetValue(ClaimName, out var value)) - throw new PasetoTokenValidationException($"Claim '{ClaimName}' not found"); - - DateTime nbf; - try - { - nbf = Convert.ToDateTime(value); - } - catch (Exception) - { - throw new PasetoTokenValidationException($"Claim '{ClaimName}' must be a DateTime"); - } - - expected ??= DateTime.UtcNow; - - if (Comparer.GetComparisonResult(nbf, expected) >= 0) // expected <= nbf + if (Comparer.GetComparisonResult(value, expected) >= 0) // expected <= nbf throw new PasetoTokenValidationException("Token is not yet valid"); } - - /// - /// Validates the payload against the provided optional expected value. - /// - /// The optional expected value. - /// true if the specified value is valid; otherwise, false. - public override bool IsValid(IComparable expected = null) - { - try - { - Validate(expected); - return true; - } - catch (Exception) - { - return false; - } - } } diff --git a/tests/Paseto.Tests/PasetoBuilderTests.cs b/tests/Paseto.Tests/PasetoBuilderTests.cs index e8d03d1..6388b61 100644 --- a/tests/Paseto.Tests/PasetoBuilderTests.cs +++ b/tests/Paseto.Tests/PasetoBuilderTests.cs @@ -594,4 +594,51 @@ public void ShouldSucceedOnEncodeToParsekWhenIsPublicPurposeAndSecretAsymmetricK parsek.Should().StartWith($"k{(int)version}.secret"); parsek.Split('.').Should().HaveCount(3); } + + [Theory(DisplayName = "Should succeed on Decoding with Date Validations")] + [InlineData(ProtocolVersion.V1)] + [InlineData(ProtocolVersion.V2)] + [InlineData(ProtocolVersion.V3)] + [InlineData(ProtocolVersion.V4)] + public void ShouldSucceedOnDecodingWithDateValidations(ProtocolVersion version) + { + const Purpose purpose = Purpose.Public; + var keyLength = version switch + { + ProtocolVersion.V1 => 0, + ProtocolVersion.V2 => 32, + ProtocolVersion.V3 => 32, + ProtocolVersion.V4 => 32, + _ => throw new ArgumentOutOfRangeException(nameof(version)) + }; + + var sharedKey = new byte[keyLength]; + RandomNumberGenerator.Fill(sharedKey); + + var keyPair = new PasetoBuilder() + .Use(version, purpose) + .GenerateAsymmetricKeyPair(sharedKey); + + var now = DateTime.UtcNow; + + var encoded = new PasetoBuilder() + .Use(version, purpose) + .WithSecretKey([.. keyPair.SecretKey.Key.Span]) + .IssuedAt(now.AddSeconds(-10)) + .Expiration(now.AddHours(1)) + .ValidFrom(now.AddSeconds(-10)) + .Encode(); + + var validationParameters = new PasetoTokenValidationParameters + { + ValidateLifetime = true + }; + + var decoded = new PasetoBuilder() + .Use(version, purpose) + .WithPublicKey([.. keyPair.PublicKey.Key.Span]) + .Decode(encoded, validationParameters); + + decoded.IsValid.Should().BeTrue(); + } }