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