diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index ec55755..c02d4a8 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -16,8 +16,11 @@ permissions:
contents: read
jobs:
default-features:
- name: Default Features (batteries_included + v4)
+ name: Default Features (batteries_included + v4 + (${{ matrix.backend }}))
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ backend: [time, chrono]
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@@ -28,13 +31,10 @@ jobs:
components: clippy
- name: Install nextest
uses: taiki-e/install-action@nextest
- - name: Clippy (default features)
- uses: actions-rs/cargo@v1
- with:
- command: clippy
- args: -- -D warnings
- - name: Test (default features)
- run: cargo nextest run
+ - name: Clippy (default features + ${{ matrix.backend }})
+ run: cargo clippy --features ${{ matrix.backend }} -- -D warnings
+ - name: Test (default features + ${{ matrix.backend }})
+ run: cargo nextest run --features ${{ matrix.backend }}
test:
name: Test Suite
runs-on: ubuntu-latest
@@ -58,8 +58,10 @@ jobs:
override: true
- name: Install nextest
uses: taiki-e/install-action@nextest
- - name: Run tests
- run: cargo nextest run --no-default-features --features ${{ matrix.feature }}
+ - name: Run tests (time)
+ run: cargo nextest run --no-default-features --features ${{ matrix.feature }},time
+ - name: Run tests (chrono)
+ run: cargo nextest run --no-default-features --features ${{ matrix.feature }},chrono
clippy:
name: Clippy
runs-on: ubuntu-latest
@@ -82,10 +84,16 @@ jobs:
toolchain: stable
override: true
components: clippy
- - uses: actions-rs/cargo@v1
+ - name: Clippy (time)
+ uses: actions-rs/cargo@v1
+ with:
+ command: clippy
+ args: --no-default-features --features ${{ matrix.feature }},time -- -D warnings
+ - name: Clippy (chrono)
+ uses: actions-rs/cargo@v1
with:
command: clippy
- args: --no-default-features --features ${{ matrix.feature }} -- -D warnings
+ args: --no-default-features --features ${{ matrix.feature }},chrono -- -D warnings
audit:
name: Security Audit
runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index ccdef24..ed27a61 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,9 @@ v4_public = ["v4", "public", "core", "ed25519-dalek", "ring/std"]
core = []
generic = ["core", "serde", "erased-serde", "serde_json"]
batteries_included = ["generic"]
+# Time backend features (mutually exclusive)
+time = ["dep:time"]
+chrono = ["dep:chrono"]
default = ["batteries_included", "v4_local", "v4_public"]
paserk = ["dep:paserk"]
@@ -78,7 +81,8 @@ hmac = { version = "0.12", optional = true }
sha2 = { version = "0.10", optional = true }
subtle = "2.6"
zeroize = { version = "1.8", features = ["zeroize_derive"] }
-time = { version = "0.3.47", features = ["parsing", "formatting"] }
+time = { version = "0.3.47", features = ["parsing", "formatting"], optional = true }
+chrono = { version = "0.4", features = ["std"], optional = true }
rand_core = "0.9"
digest = "0.10"
paserk = { version = "0.4.0", optional = true }
diff --git a/src/error.rs b/src/error.rs
index 4358ce0..6b24a96 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -136,8 +136,14 @@ pub enum Error {
// ==================== Time Format Errors ====================
/// Error formatting time values
+ #[cfg(feature = "time")]
#[error("time format error: {0}")]
TimeFormat(#[from] time::error::Format),
+
+ /// Error parsing time values
+ #[cfg(feature = "chrono")]
+ #[error("chrono parse error: {0}")]
+ ChronoParse(#[from] chrono::ParseError),
}
/// A specialized Result type for rusty_paseto operations.
diff --git a/src/generic/claims/mod.rs b/src/generic/claims/mod.rs
index 7f7e00c..e0d7b30 100644
--- a/src/generic/claims/mod.rs
+++ b/src/generic/claims/mod.rs
@@ -32,15 +32,26 @@ mod unit_tests {
//TODO: need more comprehensive tests than these to flesh out the additionl error types
use super::*;
use anyhow::Result;
- //use chrono::prelude::*;
use std::convert::TryFrom;
+
+ #[cfg(feature = "time")]
use time::format_description::well_known::Rfc3339;
+ #[cfg(feature = "time")]
+ fn now_rfc3339() -> String {
+ time::OffsetDateTime::now_utc().format(&Rfc3339).expect("format failed")
+ }
+
+ #[cfg(feature = "chrono")]
+ fn now_rfc3339() -> String {
+ chrono::Utc::now().to_rfc3339()
+ }
+
#[test]
fn test_expiration_claim() -> Result<()> {
// setup
// a good time format
- let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?;
+ let now = now_rfc3339();
assert!(ExpirationClaim::try_from("hello").is_err());
let claim = ExpirationClaim::try_from(now);
@@ -56,7 +67,7 @@ mod unit_tests {
fn test_not_before_claim() -> Result<()> {
// setup
// a good time format
- let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?;
+ let now = now_rfc3339();
assert!(NotBeforeClaim::try_from("hello").is_err());
let claim = NotBeforeClaim::try_from(now);
@@ -72,7 +83,7 @@ mod unit_tests {
fn test_issued_at_claim() -> Result<()> {
// setup
// a good time format
- let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?;
+ let now = now_rfc3339();
assert!(IssuedAtClaim::try_from("hello").is_err());
let claim = IssuedAtClaim::try_from(now);
diff --git a/src/lib.rs b/src/lib.rs
index 8532d8e..286501c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -36,7 +36,7 @@
//!
//!
//! # Usage
-//! `rusty_paseto` is meant to be flexible and configurable for your specific use case. Whether you want to get started quickly with sensible defaults, create your own version of `rusty_paseto` in order to customize your own defaults and functionality or just want to use the core PASETO crypto features, the crate is heavily feature gated to allow for your needs.
+//! `rusty_paseto` is meant to be flexible and configurable for your specific use case. Whether you want to get started quickly with sensible defaults, create your own version of `rusty_paseto` in order to customize your own defaults and functionality or just want to use the core PASETO crypto features, the crate is heavily feature gated to allow for your needs.
//! ## Architecture
@@ -164,7 +164,7 @@
//! ```
//! ### core
//!
-//! The core architectural layer is the most basic PASETO implementation as it accepts a Payload, optional Footer and (if v3 or v4) an optional Implicit Assertion along with the appropriate key to encrypt/sign and decrypt/verify basic strings.
+//! The core architectural layer is the most basic PASETO implementation as it accepts a Payload, optional Footer and (if v3 or v4) an optional Implicit Assertion along with the appropriate key to encrypt/sign and decrypt/verify basic strings.
//!
//!
//!
@@ -386,9 +386,13 @@
//! // must include
//! use std::convert::TryFrom;
//! let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
-//! // real-world example using the time crate to expire 5 minutes from now
+//! // real-world example: expire 5 minutes from now
+//! # #[cfg(feature = "time")]
//! # use time::format_description::well_known::Rfc3339;
+//! # #[cfg(feature = "time")]
//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?;
+//! # #[cfg(feature = "chrono")]
+//! # let in_5_minutes = (chrono::Utc::now() + chrono::Duration::minutes(5)).to_rfc3339();
//!
//! let token = PasetoBuilder::::default()
//! // note the TryFrom implmentation for ExpirationClaim
@@ -406,7 +410,7 @@
//!
//! A **1 hour** `ExpirationClaim` is set by default because the use case for non-expiring tokens in the world of security tokens is fairly limited.
//! Omitting an expiration claim or forgetting to require one when processing them
-//! is almost certainly an oversight rather than a deliberate choice.
+//! is almost certainly an oversight rather than a deliberate choice.
//!
//! When it is a deliberate choice, you have the opportunity to deliberately remove this claim from the Builder.
//! The method call required to do so ensures readers of the code understand the implicit risk.
@@ -414,10 +418,14 @@
//! # #[cfg(feature = "default")]
//! # {
//! # use rusty_paseto::prelude::*;
-//! # use time::format_description::well_known::Rfc3339;
//! # use std::convert::TryFrom;
-//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?;
//! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
+//! # #[cfg(feature = "time")]
+//! # use time::format_description::well_known::Rfc3339;
+//! # #[cfg(feature = "time")]
+//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?;
+//! # #[cfg(feature = "chrono")]
+//! # let in_5_minutes = (chrono::Utc::now() + chrono::Duration::minutes(5)).to_rfc3339();
//! let token = PasetoBuilder::::default()
//! .set_claim(ExpirationClaim::try_from(in_5_minutes)?)
//! // even if you set an expiration claim (as above) it will be ignored
@@ -435,15 +443,20 @@
//! # #[cfg(all(test,feature = "v4_local"))]
//! # {
//! # use rusty_paseto::prelude::*;
-//! # use time::format_description::well_known::Rfc3339;
//! # // must include
//! # use std::convert::TryFrom;
//! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
-//! # // real-world example using the time crate to expire 5 minutes from now
+//! # #[cfg(feature = "time")]
+//! # use time::format_description::well_known::Rfc3339;
+//! # #[cfg(feature = "time")]
//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?;
-//! // real-world example using the time crate to prevent the token from being used before 2
-//! // minutes from now
+//! # #[cfg(feature = "chrono")]
+//! # let in_5_minutes = (chrono::Utc::now() + chrono::Duration::minutes(5)).to_rfc3339();
+//! // set not-before to 2 minutes in the future
+//! # #[cfg(feature = "time")]
//! let in_2_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(2)).format(&Rfc3339)?;
+//! # #[cfg(feature = "chrono")]
+//! # let in_2_minutes = (chrono::Utc::now() + chrono::Duration::minutes(2)).to_rfc3339();
//!
//! let token = PasetoBuilder::::default()
//! //json payload key: "exp"
@@ -671,6 +684,13 @@ compile_error!(
See: https://github.com/rrrodzilla/rusty_paseto/issues/48"
);
+// Ensure exactly one time backend is selected when the prelude is used.
+#[cfg(all(feature = "time", feature = "chrono"))]
+compile_error!("features `time` and `chrono` are mutually exclusive. Enable exactly one time backend");
+
+#[cfg(all(not(feature = "time"), not(feature = "chrono")))]
+compile_error!("a time backend is required. Enable either the `time` feature or `chrono`");
+
//public interface
#[cfg(feature = "core")]
pub mod core;
diff --git a/src/prelude/error.rs b/src/prelude/error.rs
index 3d41c3a..c6db6cc 100644
--- a/src/prelude/error.rs
+++ b/src/prelude/error.rs
@@ -21,5 +21,10 @@ pub enum GeneralPasetoError {
},
/// An error with the data format
#[error(transparent)]
+ #[cfg(feature = "time")]
RFC3339Date(#[from] time::error::Format),
+ /// An error parsing a date/time value with chrono
+ #[error(transparent)]
+ #[cfg(feature = "chrono")]
+ ChronoParse(#[from] chrono::ParseError),
}
diff --git a/src/prelude/paseto_builder.rs b/src/prelude/paseto_builder.rs
index 5beae30..ca0874e 100644
--- a/src/prelude/paseto_builder.rs
+++ b/src/prelude/paseto_builder.rs
@@ -2,8 +2,10 @@ use crate::generic::*;
use core::marker::PhantomData;
use std::collections::HashSet;
use std::convert::TryFrom;
+
+#[cfg(feature = "time")]
use time::format_description::well_known::Rfc3339;
- ///The `PasetoBuilder` is created at compile time by specifying a PASETO version and purpose and
+///The `PasetoBuilder` is created at compile time by specifying a PASETO version and purpose and
///providing a key of the same version and purpose. This structure allows setting [PASETO claims](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md),
///your own [custom claims](CustomClaim), an optional [footer](Footer) and in the case of V3/V4 tokens, an optional [implicit
///assertion](ImplicitAssertion).
@@ -19,9 +21,9 @@ use time::format_description::well_known::Rfc3339;
///# #[cfg(all(feature = "prelude", feature="v2_local"))]
///# {
/// use rusty_paseto::prelude::*;
- /// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- /// let footer = Footer::from("some footer");
- /// //create a builder, add some claims and then build the token with the key
+/// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub"));
+/// let footer = Footer::from("some footer");
+/// //create a builder, add some claims and then build the token with the key
/// let token = PasetoBuilder::::default()
/// .set_claim(AudienceClaim::from("customers"))
/// .set_claim(SubjectClaim::from("loyal subjects"))
@@ -35,11 +37,11 @@ use time::format_description::well_known::Rfc3339;
/// .set_claim(CustomClaim::try_from(("pi to 6 digits", 3.141526))?)
/// .set_footer(footer)
/// .try_encrypt(&key)?;
- /// //now let's decrypt the token and verify the values
+/// //now let's decrypt the token and verify the values
/// let json = PasetoParser::::default()
/// .set_footer(footer)
/// .parse(&token, &key)?;
- /// assert_eq!(json["aud"], "customers");
+/// assert_eq!(json["aud"], "customers");
/// assert_eq!(json["jti"], "me");
/// assert_eq!(json["iss"], "me");
/// assert_eq!(json["data"], "this is a secret message");
@@ -72,7 +74,7 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> {
dup_top_level_found: (false, String::default()),
}
}
- /// Given a [`PasetoClaim`], attempts to add it to the builder for inclusion in the payload of the
+ /// Given a [`PasetoClaim`], attempts to add it to the builder for inclusion in the payload of the
/// token.
/// claims provided to the `GenericBuilder`. Overwrites the default 'nbf' (not before) claim if
/// provided. Prevents duplicate claims from being added.
@@ -113,10 +115,10 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> {
self.builder.set_claim(value);
self
}
- /// Sets the token to have no expiration date.
+ /// Sets the token to have no expiration date.
/// A **1 hour** `ExpirationClaim` is set by default because the use case for non-expiring tokens in the world of security tokens is fairly limited.
/// Omitting an expiration claim or forgetting to require one when processing them
- /// is almost certainly an oversight rather than a deliberate choice.
+ /// is almost certainly an oversight rather than a deliberate choice.
/// When it is a deliberate choice, you have the opportunity to deliberately remove this claim from the Builder.
/// This method call ensures readers of the code understand the implicit risk.
///
@@ -145,7 +147,7 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> {
self.non_expiring_token = true;
self
}
- /// Sets an optional [Footer] on the token.
+ /// Sets an optional [Footer] on the token.
///
/// Returns a mutable reference to the builder on success.
///
@@ -304,18 +306,27 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> {
/// # Ok::<(),anyhow::Error>(())
/// ```
pub fn expires_in(mut self, duration: std::time::Duration) -> Self {
- let now = time::OffsetDateTime::now_utc();
- // Convert std::time::Duration to time::Duration
- let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::hours(1));
-
- if let Some(expiration) = now.checked_add(time_duration) {
- if let Ok(formatted) = expiration.format(&Rfc3339) {
- if let Ok(exp_claim) = ExpirationClaim::try_from(formatted) {
- // Remove existing expiration claim and add new one
- self.builder.remove_claim("exp");
- self.top_level_claims.remove("exp");
- self.set_claim(exp_claim);
- }
+ #[cfg(feature = "time")]
+ let formatted: Option = {
+ let now = time::OffsetDateTime::now_utc();
+ // Convert std::time::Duration to time::Duration
+ let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::hours(1));
+ now.checked_add(time_duration).and_then(|t| t.format(&Rfc3339).ok())
+ };
+ #[cfg(feature = "chrono")]
+ let formatted: Option = {
+ let now = chrono::Utc::now();
+ // Convert std::time::Duration to chrono::Duration
+ let chrono_duration = chrono::Duration::from_std(duration).unwrap_or(chrono::Duration::hours(1));
+ Some((now + chrono_duration).to_rfc3339())
+ };
+
+ if let Some(f) = formatted {
+ if let Ok(expiration) = ExpirationClaim::try_from(f) {
+ // Remove existing expiration claim and add new one
+ self.builder.remove_claim("exp");
+ self.top_level_claims.remove("exp");
+ self.set_claim(expiration);
}
}
self
@@ -340,17 +351,26 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> {
/// # Ok::<(),anyhow::Error>(())
/// ```
pub fn not_before_in(mut self, duration: std::time::Duration) -> Self {
- let now = time::OffsetDateTime::now_utc();
- // Convert std::time::Duration to time::Duration
- let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::ZERO);
-
- if let Some(not_before) = now.checked_add(time_duration) {
- if let Ok(formatted) = not_before.format(&Rfc3339) {
- if let Ok(nbf_claim) = NotBeforeClaim::try_from(formatted) {
- // Remove existing nbf claim and add new one
- self.builder.remove_claim("nbf");
- self.set_claim(nbf_claim);
- }
+ #[cfg(feature = "time")]
+ let formatted: Option = {
+ let now = time::OffsetDateTime::now_utc();
+ // Convert std::time::Duration to time::Duration
+ let time_duration = time::Duration::try_from(duration).unwrap_or(time::Duration::ZERO);
+ now.checked_add(time_duration).and_then(|t| t.format(&Rfc3339).ok())
+ };
+ #[cfg(feature = "chrono")]
+ let formatted: Option = {
+ let now = chrono::Utc::now();
+ // Convert std::time::Duration to chrono::Duration
+ let chrono_duration = chrono::Duration::from_std(duration).unwrap_or(chrono::Duration::zero());
+ Some((now + chrono_duration).to_rfc3339())
+ };
+
+ if let Some(f) = formatted {
+ if let Ok(nbf_claim) = NotBeforeClaim::try_from(f) {
+ // Remove existing nbf claim and add new one
+ self.builder.remove_claim("nbf");
+ self.set_claim(nbf_claim);
}
}
self
@@ -400,19 +420,39 @@ where
impl<'a, Version, Purpose> Default for PasetoBuilder<'a, Version, Purpose> {
fn default() -> Self {
let mut new_builder = Self::new();
- let now = time::OffsetDateTime::now_utc();
-
- // Use checked_add to handle potential overflow (though unlikely with 1 hour duration)
- // RFC3339 formatting of valid OffsetDateTime values should be infallible
- // but we handle potential failures gracefully by skipping claim creation
- if let Some(in_one_hour) = now.checked_add(time::Duration::hours(1)) {
- if let Ok(expiration_time) = in_one_hour.format(&Rfc3339) {
- if let Ok(exp_claim) = ExpirationClaim::try_from(expiration_time) {
- new_builder.builder.set_claim(exp_claim);
+
+ #[cfg(feature = "time")]
+ {
+ let now = time::OffsetDateTime::now_utc();
+
+ // Use checked_add to handle potential overflow (though unlikely with 1 hour duration)
+ // RFC3339 formatting of valid OffsetDateTime values should be infallible
+ // but we handle potential failures gracefully by skipping claim creation
+ if let Some(in_one_hour) = now.checked_add(time::Duration::hours(1)) {
+ if let Ok(expiration_time) = in_one_hour.format(&Rfc3339) {
+ if let Ok(exp_claim) = ExpirationClaim::try_from(expiration_time) {
+ new_builder.builder.set_claim(exp_claim);
+ }
+ }
+ }
+ if let Ok(current_time) = now.format(&Rfc3339) {
+ if let Ok(iat_claim) = IssuedAtClaim::try_from(current_time.clone()) {
+ new_builder.builder.set_claim(iat_claim);
+ }
+ if let Ok(nbf_claim) = NotBeforeClaim::try_from(current_time) {
+ new_builder.builder.set_claim(nbf_claim);
}
}
}
- if let Ok(current_time) = now.format(&Rfc3339) {
+
+ #[cfg(feature = "chrono")]
+ {
+ let now = chrono::Utc::now();
+ let in_one_hour = now + chrono::Duration::hours(1);
+ if let Ok(exp_claim) = ExpirationClaim::try_from(in_one_hour.to_rfc3339()) {
+ new_builder.builder.set_claim(exp_claim);
+ }
+ let current_time = now.to_rfc3339();
if let Ok(iat_claim) = IssuedAtClaim::try_from(current_time.clone()) {
new_builder.builder.set_claim(iat_claim);
}
@@ -440,7 +480,7 @@ impl PasetoBuilder<'_, V1, Local> {
///# {
/// use rusty_paseto::prelude::*;
/// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- /// let footer = Footer::from("some footer");
+ /// let footer = Footer::from("some footer");
/// //create a builder, add some claims and then build the token with the key
/// let token = PasetoBuilder::::default()
/// .set_claim(AudienceClaim::from("customers"))
@@ -495,7 +535,7 @@ impl PasetoBuilder<'_, V2, Local> {
///# {
/// use rusty_paseto::prelude::*;
/// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- /// let footer = Footer::from("some footer");
+ /// let footer = Footer::from("some footer");
/// //create a builder, add some claims and then build the token with the key
/// let token = PasetoBuilder::::default()
/// .set_claim(AudienceClaim::from("customers"))
@@ -549,7 +589,7 @@ impl PasetoBuilder<'_, V3, Local> {
///# {
/// use rusty_paseto::prelude::*;
/// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- /// let footer = Footer::from("some footer");
+ /// let footer = Footer::from("some footer");
/// let implicit_assertion = ImplicitAssertion::from("some assertion");
/// //create a builder, add some claims and then build the token with the key
/// let token = PasetoBuilder::::default()
@@ -606,7 +646,7 @@ impl PasetoBuilder<'_, V4, Local> {
///# {
/// use rusty_paseto::prelude::*;
/// let key = PasetoSymmetricKey::::from(Key::<32>::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- /// let footer = Footer::from("some footer");
+ /// let footer = Footer::from("some footer");
/// let implicit_assertion = ImplicitAssertion::from("some assertion");
/// //create a builder, add some claims and then build the token with the key
/// let token = PasetoBuilder::::default()
@@ -672,7 +712,7 @@ impl PasetoBuilder<'_, V1, Public> {
/// #[allow(deprecated)]
/// let private_key = PasetoAsymmetricPrivateKey::::from(pk);
/// let footer = Footer::from("some footer");
- /// //sign a public V1 token
+ /// //sign a public V1 token
/// #[allow(deprecated)]
/// let token = PasetoBuilder::::default()
/// .set_claim(AudienceClaim::from("customers"))
@@ -722,8 +762,8 @@ impl PasetoBuilder<'_, V1, Public> {
/// # Ok::<(),anyhow::Error>(())
///```
#[deprecated(
- since = "0.8.1",
- note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code."
+ since = "0.8.1",
+ note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code."
)]
#[allow(deprecated)]
pub fn build(&mut self, key: &PasetoAsymmetricPrivateKey) -> Result {
@@ -752,7 +792,7 @@ impl PasetoBuilder<'_, V2, Public> {
/// let public_key = Key::<32>::try_from("1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2")?;
/// let public_key = PasetoAsymmetricPublicKey::::from(&public_key);
/// let footer = Footer::from("some footer");
- /// //sign a public V2 token
+ /// //sign a public V2 token
/// let token = PasetoBuilder::::default()
/// .set_claim(AudienceClaim::from("customers"))
/// .set_claim(SubjectClaim::from("loyal subjects"))
@@ -824,7 +864,7 @@ impl PasetoBuilder<'_, V3, Public> {
/// )?;
/// let public_key = PasetoAsymmetricPublicKey::::try_from(&public_key)?;
/// let footer = Footer::from("some footer");
- /// let implicit_assertion = ImplicitAssertion::from("some assertion");
+ /// let implicit_assertion = ImplicitAssertion::from("some assertion");
/// //sign a public V3 token
/// let token = PasetoBuilder::::default()
/// .set_claim(AudienceClaim::from("customers"))
@@ -953,14 +993,37 @@ mod paseto_builder {
use crate::prelude::*;
use anyhow::Result;
use std::convert::TryFrom;
+
+ #[cfg(feature = "time")]
use time::format_description::well_known::Rfc3339;
+ // Helper: RFC3339 string for now + N seconds (positive offset only).
+ // For past datetimes use a hardcoded string.
+ #[cfg(feature = "time")]
+ fn rfc3339_from_now_plus_secs(secs: i64) -> String {
+ let d = time::Duration::seconds(secs);
+ (time::OffsetDateTime::now_utc() + d)
+ .format(&Rfc3339)
+ .expect("format failed")
+ }
+
+ #[cfg(feature = "chrono")]
+ fn rfc3339_from_now_plus_secs(secs: i64) -> String {
+ let d = chrono::Duration::seconds(secs);
+ (chrono::Utc::now() + d).to_rfc3339()
+ }
+
+ // Helper: date string (YYYY-MM-DD) for now + N seconds.
+ fn date_from_rfc3339(s: &str) -> String {
+ iso8601::datetime(s).expect("iso8601 parse failed").date.to_string()
+ }
+
#[test]
fn duplicate_top_level_claim_test() -> Result<()> {
//create a key
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?;
+ let tomorrow = rfc3339_from_now_plus_secs(86400);
//let tomorrow = (Utc::now() + Duration::days(1)).to_rfc3339();
@@ -987,7 +1050,7 @@ mod paseto_builder {
//create a key
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?;
+ let tomorrow = rfc3339_from_now_plus_secs(86400);
//create a builder, with default IssuedAtClaim
let token = PasetoBuilder::::default()
@@ -1008,7 +1071,7 @@ mod paseto_builder {
//create a key
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?;
+ let tomorrow = rfc3339_from_now_plus_secs(86400);
//create a builder, with default IssuedAtClaim
let token = PasetoBuilder::::default()
@@ -1025,9 +1088,7 @@ mod paseto_builder {
.ok_or_else(|| PasetoClaimError::Unexpected(key.to_string()))?;
let datetime = iso8601::datetime(val).unwrap();
- let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1))
- .date()
- .to_string();
+ let tomorrow = date_from_rfc3339(&rfc3339_from_now_plus_secs(86400));
//the claimm should exist
assert_eq!(key, "iat");
@@ -1060,7 +1121,7 @@ mod paseto_builder {
let datetime = iso8601::datetime(val).unwrap();
- let now = time::OffsetDateTime::now_utc().date().to_string();
+ let now = date_from_rfc3339(&rfc3339_from_now_plus_secs(0));
//the claimm should exist
assert_eq!(key, "iat");
//date should be today
@@ -1079,7 +1140,7 @@ mod paseto_builder {
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
//let in_4_days = (Utc::now() + Duration::days(4)).to_rfc3339();
- let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4)).format(&Rfc3339)?;
+ let in_4_days = rfc3339_from_now_plus_secs(4 * 86400);
//create a builder, with default IssuedAtClaim
let token = PasetoBuilder::::default()
@@ -1097,10 +1158,7 @@ mod paseto_builder {
let datetime = iso8601::datetime(val).unwrap();
- let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4))
- .date()
- .to_string();
- //let in_4_days = Utc::now() + Duration::days(4);
+ let in_4_days = date_from_rfc3339(&rfc3339_from_now_plus_secs(4 * 86400));
//the claimm should exist
assert_eq!(key, "exp");
//date should be tomorrow
@@ -1131,9 +1189,7 @@ mod paseto_builder {
.ok_or_else(|| PasetoClaimError::Unexpected(key.to_string()))?;
let datetime = iso8601::datetime(val).unwrap();
- let expires = (time::OffsetDateTime::now_utc() + time::Duration::hours(1))
- .date()
- .to_string();
+ let expires = date_from_rfc3339(&rfc3339_from_now_plus_secs(3600));
//let tomorrow = Utc::now() + Duration::hours(1);
//the claimm should exist
diff --git a/src/prelude/paseto_parser.rs b/src/prelude/paseto_parser.rs
index ff0d20a..3519084 100644
--- a/src/prelude/paseto_parser.rs
+++ b/src/prelude/paseto_parser.rs
@@ -1,6 +1,8 @@
use crate::generic::*;
use core::marker::PhantomData;
use serde_json::Value;
+
+#[cfg(feature = "time")]
use time::format_description::well_known::Rfc3339;
///The `PasetoParser` validates and parses PASETO tokens. Created at compile time by specifying a PASETO version and purpose.
///
@@ -118,13 +120,18 @@ impl<'a, Version, Purpose> PasetoParser<'a, Version, Purpose> {
/// //let's get the value
/// let val = value.as_str().ok_or(PasetoClaimError::Unexpected(key.to_string()))?;
/// let datetime = iso8601::datetime(val).unwrap();
+ /// # #[cfg(feature = "time")]
/// let in_an_hour = (time::OffsetDateTime::now_utc() + time::Duration::hours(1))
/// .time()
/// .hour()
/// .to_string();
+ /// # #[cfg(feature = "chrono")]
+ /// # let in_an_hour = (chrono::Utc::now() + chrono::Duration::hours(1))
+ /// # .format("%H")
+ /// # .to_string();
/// //the claimm should exist
/// assert_eq!(key, "exp");
- /// //date should be today
+ /// //hour should match expected
/// assert_eq!(datetime.time.hour.to_string(), in_an_hour);
/// Ok(())
/// })
@@ -220,10 +227,19 @@ impl<'a, Version, Purpose> PasetoParser<'a, Version, Purpose> {
if val.is_empty() {
return Ok(());
}
- let datetime = time::OffsetDateTime::parse(val, &Rfc3339)
- .map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
- let now = time::OffsetDateTime::now_utc();
- if datetime <= now {
+ #[cfg(feature = "time")]
+ let expired: bool = {
+ let datetime =
+ time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
+ datetime <= time::OffsetDateTime::now_utc()
+ };
+ #[cfg(feature = "chrono")]
+ let expired: bool = {
+ let datetime =
+ chrono::DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
+ datetime <= chrono::Utc::now()
+ };
+ if expired {
Err(PasetoClaimError::Expired)
} else {
Ok(())
@@ -345,14 +361,20 @@ impl<'a, Version, Purpose> Default for PasetoParser<'a, Version, Purpose> {
if val.is_empty() {
return Err(PasetoClaimError::Missing("exp".to_string()));
}
- //turn the value into a datetime
- let datetime =
- time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
- //get the current datetime
- let now = time::OffsetDateTime::now_utc();
-
- //here we do the actual validation check for the expiration claim
- if datetime <= now {
+ #[cfg(feature = "time")]
+ let expired: bool = {
+ //turn the value into a datetime
+ let datetime =
+ time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
+ datetime <= time::OffsetDateTime::now_utc()
+ };
+ #[cfg(feature = "chrono")]
+ let expired: bool = {
+ let datetime =
+ chrono::DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
+ datetime <= chrono::Utc::now()
+ };
+ if expired {
Err(PasetoClaimError::Expired)
} else {
Ok(())
@@ -366,18 +388,38 @@ impl<'a, Version, Purpose> Default for PasetoParser<'a, Version, Purpose> {
return Ok(());
}
//otherwise let's continue with the validation
+ #[cfg(feature = "time")]
//turn the value into a datetime
- let not_before_time =
- time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
- //get the current datetime
- let now = time::OffsetDateTime::now_utc();
-
- //here we do the actual validation check for the not-before claim.
- //RFC 7519 §4.1.5: token is valid when `now >= nbf`, so reject only when
- //`now < nbf`. Using strict `<` here (not `<=`) so that a token with
- //nbf == now is accepted as soon as its activation instant arrives.
- if now < not_before_time {
- Err(PasetoClaimError::UseBeforeAvailable(not_before_time.to_string()))
+ let not_before_str: Option = {
+ let not_before_time =
+ time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
+ //get the current datetime
+ let now = time::OffsetDateTime::now_utc();
+
+ //here we do the actual validation check for the not-before claim.
+ //RFC 7519 §4.1.5: token is valid when `now >= nbf`, so reject only when
+ //`now < nbf`. Using strict `<` here (not `<=`) so that a token with
+ //nbf == now is accepted as soon as its activation instant arrives.
+ if now < not_before_time {
+ Some(not_before_time.to_string())
+ } else {
+ None
+ }
+ };
+ #[cfg(feature = "chrono")]
+ let not_before_str: Option = {
+ let not_before_time =
+ chrono::DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?;
+ let now = chrono::Utc::now();
+ if now < not_before_time {
+ Some(not_before_time.to_string())
+ } else {
+ None
+ }
+ };
+ //RFC 7519 §4.1.5: reject only when now < nbf (strict <, so nbf == now is accepted).
+ if let Some(not_before_str) = not_before_str {
+ Err(PasetoClaimError::UseBeforeAvailable(not_before_str))
} else {
Ok(())
}
@@ -777,8 +819,8 @@ impl<'a> PasetoParser<'a, V1, Public> {
/// # Ok::<(),anyhow::Error>(())
///```
#[deprecated(
- since = "0.8.1",
- note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code."
+ since = "0.8.1",
+ note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code."
)]
#[allow(deprecated)]
pub fn parse(
@@ -794,8 +836,8 @@ impl<'a> PasetoParser<'a, V1, Public> {
///
/// See [`PasetoParser::parse_into`] for detailed documentation.
#[deprecated(
- since = "0.8.1",
- note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code."
+ since = "0.8.1",
+ note = "V1 is the legacy PASETO version (2048-bit RSA-PSS). PASETO spec recommends V4 for new code."
)]
#[allow(deprecated)]
pub fn parse_into(
@@ -1123,9 +1165,27 @@ mod paseto_parser_unit_tests {
use crate::prelude::*;
use anyhow::Result;
- #[cfg(feature = "v2_local")]
+
+ #[cfg(feature = "time")]
use time::format_description::well_known::Rfc3339;
+ #[cfg(feature = "time")]
+ fn rfc3339_from_now_plus_secs(secs: i64) -> String {
+ let d = time::Duration::seconds(secs);
+ (time::OffsetDateTime::now_utc() + d)
+ .format(&Rfc3339)
+ .expect("format failed")
+ }
+
+ #[cfg(feature = "chrono")]
+ fn rfc3339_from_now_plus_secs(secs: i64) -> String {
+ (chrono::Utc::now() + chrono::Duration::seconds(secs)).to_rfc3339()
+ }
+
+ fn date_from_rfc3339(s: &str) -> String {
+ iso8601::datetime(s).expect("iso8601 parse failed").date.to_string()
+ }
+
#[cfg(feature = "v2_local")]
#[test]
fn usage_before_ready_test() -> Result<()> {
@@ -1133,7 +1193,7 @@ mod paseto_parser_unit_tests {
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
//let not_before = Utc::now() + Duration::hours(1);
- let not_before = (time::OffsetDateTime::now_utc() + time::Duration::hours(1)).format(&Rfc3339)?;
+ let not_before = rfc3339_from_now_plus_secs(3600);
//create a default builder
let token = PasetoBuilder::::default()
.set_claim(NotBeforeClaim::try_from(not_before)?)
@@ -1154,7 +1214,7 @@ mod paseto_parser_unit_tests {
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
//we're going to set a token expiration date to 10 minutes ago
- let expired = (time::OffsetDateTime::now_utc() + time::Duration::minutes(-10)).format(&Rfc3339)?;
+ let expired = rfc3339_from_now_plus_secs(-600);
//create a default builder
let token = PasetoBuilder::::default()
@@ -1195,10 +1255,7 @@ mod paseto_parser_unit_tests {
//The default parser must reject it.
let result = PasetoParser::::default().parse(&token, &key);
- assert!(
- result.is_err(),
- "default parser must reject a token with no exp claim",
- );
+ assert!(result.is_err(), "default parser must reject a token with no exp claim",);
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("exp"),
@@ -1219,7 +1276,7 @@ mod paseto_parser_unit_tests {
//create a key
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- let expired = (time::OffsetDateTime::now_utc() + time::Duration::minutes(-10)).format(&Rfc3339)?;
+ let expired = rfc3339_from_now_plus_secs(-600);
//create a default builder
let token = PasetoBuilder::::default()
.set_claim(ExpirationClaim::try_from(expired)?)
@@ -1308,7 +1365,7 @@ mod paseto_parser_unit_tests {
//create a key
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?;
+ let tomorrow = rfc3339_from_now_plus_secs(86400);
//create a builder, with default IssuedAtClaim
let token = PasetoBuilder::::default()
@@ -1325,9 +1382,7 @@ mod paseto_parser_unit_tests {
let datetime = iso8601::datetime(val).unwrap();
//let tomorrow = Utc::now() + Duration::days(1);
- let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1))
- .date()
- .to_string();
+ let tomorrow = date_from_rfc3339(&rfc3339_from_now_plus_secs(86400));
//the claimm should exist
assert_eq!(key, "iat");
//date should be tomorrow
@@ -1359,7 +1414,7 @@ mod paseto_parser_unit_tests {
let datetime = iso8601::datetime(val).unwrap();
//the claimm should exist
- let now = time::OffsetDateTime::now_utc().date().to_string();
+ let now = date_from_rfc3339(&rfc3339_from_now_plus_secs(0));
assert_eq!(key, "iat");
//date should be today
assert_eq!(datetime.date.to_string(), now);
@@ -1377,7 +1432,7 @@ mod paseto_parser_unit_tests {
//create a key
let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub"));
- let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4)).format(&Rfc3339)?;
+ let in_4_days = rfc3339_from_now_plus_secs(4 * 86400);
//create a builder, with default IssuedAtClaim
let token = PasetoBuilder::::default()
@@ -1394,9 +1449,7 @@ mod paseto_parser_unit_tests {
let datetime = iso8601::datetime(val).unwrap();
//let in_4_days = Utc::now() + Duration::days(4);
- let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4))
- .date()
- .to_string();
+ let in_4_days = date_from_rfc3339(&rfc3339_from_now_plus_secs(4 * 86400));
//the claimm should exist
assert_eq!(key, "exp");
//date should be tomorrow
@@ -1427,14 +1480,11 @@ mod paseto_parser_unit_tests {
let datetime = iso8601::datetime(val).unwrap();
- let in_an_hour = (time::OffsetDateTime::now_utc() + time::Duration::hours(1))
- .time()
- .hour()
- .to_string();
+ let in_an_hour = date_from_rfc3339(&rfc3339_from_now_plus_secs(3600));
//the claimm should exist
assert_eq!(key, "exp");
- //date should be today
- assert_eq!(datetime.time.hour.to_string(), in_an_hour);
+ //date should be today (or tomorrow if we're within 1 hour of midnight)
+ assert_eq!(datetime.date.to_string(), in_an_hour);
Ok(())
})