diff --git a/Cargo.toml b/Cargo.toml index 36df12a..b55c4ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rusty_paseto" -version = "0.2.3" +version = "0.2.4" edition = "2021" readme = "readme.md" authors = ["Roland Rodriguez ::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); + // use a default token builder with the same PASETO version and purpose + let token = PasetoBuilder::::default().build(&key)?; + // token is a String in the form: "v4.local.encoded-payload" + + ``` + + ## A default token + + * Has no [footer](https://github.com/paseto-standard/paseto-spec/tree/master/docs) + * Has no [implicit assertion](https://github.com/paseto-standard/paseto-spec/tree/master/docs) + for V3 or V4 versioned tokens + * Expires in **1 hour** after creation (due to an included default ExpirationClaim) + * Contains an IssuedAtClaim defaulting to the current utc time the token was created + * Contains a NotBeforeClaim defaulting to the current utc time the token was created + + + You can parse and validate an existing token with the following: + ```Rust + let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); + // now we can parse and validate the token with a parser that returns a serde_json::Value + let json_value = PasetoParser::::default().parse(&token, &key)?; + + //the ExpirationClaim + assert!(json_value["exp"].is_string()); + //the IssuedAtClaim + assert!(json_value["iat"].is_string()); + + ``` + + ## A default parser + + * Validates the token structure and decryptes the payload or verifies the signature of the content + * Validates the [footer](https://github.com/paseto-standard/paseto-spec/tree/master/docs) if + one was provided + * Validates the [implicit assertion](https://github.com/paseto-standard/paseto-spec/tree/master/docs) if one was provided (for V3 or V4 versioned tokens only) + + ## A token with a footer + + PASETO tokens can have an [optional footer](https://github.com/paseto-standard/paseto-spec/tree/master/docs). In rusty_paseto we have strict types for most things. + So we can extend the previous example to add a footer to the token by using code like the + following: + ```rust + use rusty_paseto::prelude::*; + let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); + let token = PasetoBuilder::::default() + // note how we set the footer here + .set_footer(Footer::from("Sometimes science is more art than science")) + .build(&key)?; + + // token is now a String in the form: "v4.local.encoded-payload.footer" + + ``` + And parse it by passing in the same expected footer + ```rust + // now we can parse and validate the token with a parser that returns a serde_json::Value + let json_value = PasetoParser::::default() + .set_footer(Footer::from("Sometimes science is more art than science")) + .parse(&token, &key)?; + + //the ExpirationClaim + assert!(json_value["exp"].is_string()); + //the IssuedAtClaim + assert!(json_value["iat"].is_string()); + + ``` + + + ## A token with an implicit assertion (V3 or V4 versioned tokens only) + + Version 3 (V3) and Version 4 (V4) PASETO tokens can have an [optional implicit assertion](https://github.com/paseto-standard/paseto-spec/tree/master/docs). + So we can extend the previous example to add an implicit assertion to the token by using code like the + following: + ```rust + use rusty_paseto::prelude::*; + let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); + let token = PasetoBuilder::::default() + .set_footer(Footer::from("Sometimes science is more art than science")) + // note how we set the implicit assertion here + .set_implicit_assertion(ImplicitAssertion::from("There’s a lesson here, and I’m not going to be the one to figure it out.")) + .build(&key)?; + + // token is now a String in the form: "v4.local.encoded-payload.footer" + + ``` + And parse it by passing in the same expected implicit assertion + ```rust + // now we can parse and validate the token with a parser that returns a serde_json::Value + let json_value = PasetoParser::::default() + .set_footer(Footer::from("Sometimes science is more art than science")) + .set_implicit_assertion(ImplicitAssertion::from("There’s a lesson here, and I’m not going to be the one to figure it out.")) + .parse(&token, &key)?; + + ``` + + ## Setting a different expiration time + + As mentioned, default tokens expire **1 hour** from creation time. You can set your own + expiration time by adding an ExpirationClaim which takes an ISO 8601 (Rfc3339) compliant datetime string. + #### Note: *claims taking an ISO 8601 (Rfc3339) string use the TryFrom trait and return a Result<(),PasetoClaimError>* + ```rust + # use rusty_paseto::prelude::*; + // 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 + + let token = PasetoBuilder::::default() + // note the TryFrom implmentation for ExpirationClaim + //.set_claim(ExpirationClaim::try_from("2019-01-01T00:00:00+00:00")?) + .set_claim(ExpirationClaim::try_from(in_5_minutes)?) + .set_footer(Footer::from("Sometimes science is more art than science")) + .build(&key)?; + + // token is a String in the form: "v4.local.encoded-payload.footer" + + ``` + + + ## Tokens that never expire + + 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. + + 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. + ```rust + 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 + // due to the method call below + .set_no_expiration_danger_acknowledged() + .build(&key)?; + + ``` + + ## Setting PASETO Claims + + The PASETO specification includes [seven reserved claims](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) which you can set with their explicit types: + ```rust + // real-world example using the time crate to prevent the token from being used before 2 + // minutes from now + let in_2_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(2)).format(&Rfc3339)?; + + let token = PasetoBuilder::::default() + //json payload key: "exp" + .set_claim(ExpirationClaim::try_from(in_5_minutes)?) + //json payload key: "iat" + // the IssueAtClaim is automatically set to UTC NOW by default + // but you can override it here + // .set_claim(IssuedAtClaim::try_from("2019-01-01T00:00:00+00:00")?) + //json payload key: "nbf" + //don't use this token before two minutes after UTC NOW + .set_claim(NotBeforeClaim::try_from(in_2_minutes)?) + //json payload key: "aud" + .set_claim(AudienceClaim::from("Cromulons")) + //json payload key: "sub" + .set_claim(SubjectClaim::from("Get schwifty")) + //json payload key: "iss" + .set_claim(IssuerClaim::from("Earth Cesium-137")) + //json payload key: "jti" + .set_claim(TokenIdentifierClaim::from("Planet Music - Season 988")) + .build(&key)?; + + ``` + + ## Setting your own Custom Claims + + The CustomClaim struct takes a tuple in the form of `(key: String, value: T)` where T is any + serializable type + #### Note: *CustomClaims use the TryFrom trait and return a Result<(), PasetoClaimError> if you attempt to use one of the [reserved PASETO keys](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) in your CustomClaim* + ```rust + let token = PasetoBuilder::::default() + .set_claim(CustomClaim::try_from(("Co-star", "Morty Smith"))?) + .set_claim(CustomClaim::try_from(("Universe", 137))?) + .build(&key)?; + + ``` + This throws an error: + ```rust + // "exp" is a reserved PASETO claim key, you should use the ExpirationClaim type + let token = PasetoBuilder::::default() + .set_claim(CustomClaim::try_from(("exp", "Some expiration value"))?) + .build(&key)?; + + ``` + # Validating claims + rusty_paseto allows for flexible claim validation at parse time + + ## Checking claims + + Let's see how we can check particular claims exist with expected values. + ```rust + // use a default token builder with the same PASETO version and purpose + let token = PasetoBuilder::::default() + .set_claim(SubjectClaim::from("Get schwifty")) + .set_claim(CustomClaim::try_from(("Contestant", "Earth"))?) + .set_claim(CustomClaim::try_from(("Universe", 137))?) + .build(&key)?; + + PasetoParser::::default() + // you can check any claim even custom claims + .check_claim(SubjectClaim::from("Get schwifty")) + .check_claim(CustomClaim::try_from(("Contestant", "Earth"))?) + .check_claim(CustomClaim::try_from(("Universe", 137))?) + .parse(&token, &key)?; + + // no need for the assertions below since the check_claim methods + // above accomplish the same but at parse time! + + //assert_eq!(json_value["sub"], "Get schwifty"); + //assert_eq!(json_value["Contestant"], "Earth"); + //assert_eq!(json_value["Universe"], 137); + ``` + + # Custom validation + + What if we have more complex validation requirements? You can pass in a reference to a closure which receives + the key and value of the claim you want to validate so you can implement any validation logic + you choose. + + Let's see how we can validate our tokens only contain universes with prime numbers: + ```rust + // use a default token builder with the same PASETO version and purpose + let token = PasetoBuilder::::default() + .set_claim(SubjectClaim::from("Get schwifty")) + .set_claim(CustomClaim::try_from(("Contestant", "Earth"))?) + .set_claim(CustomClaim::try_from(("Universe", 137))?) + .build(&key)?; + + PasetoParser::::default() + .check_claim(SubjectClaim::from("Get schwifty")) + .check_claim(CustomClaim::try_from(("Contestant", "Earth"))?) + .validate_claim(CustomClaim::try_from("Universe")?, &|key, value| { + //let's get the value + let universe = value + .as_u64() + .ok_or(PasetoClaimError::Unexpected(key.to_string()))?; + // we only accept prime universes in this app + if primes::is_prime(universe) { + Ok(()) + } else { + Err(PasetoClaimError::CustomValidation(key.to_string())) + } + }) + .parse(&token, &key)?; + + ``` + + This token will fail to parse with the validation code above: + ```rust + // 136 is not a prime number + let token = PasetoBuilder::::default() + .set_claim(CustomClaim::try_from(("Universe", 136))?) + .build(&key)?; + + ``` + + # Acknowledgments + + If the API of this crate doesn't suit your tastes, check out the other PASETO implementations + in the Rust ecosystem which inspired rusty_paseto: + + - [paseto](https://crates.io/crates/paseto) - by [Cynthia Coan](https://crates.io/users/Mythra) + - [pasetors](https://crates.io/crates/pasetors) - by [Johannes](https://crates.io/users/brycx) + diff --git a/src/generic/claims/mod.rs b/src/generic/claims/mod.rs index 4ff5abd..4966346 100644 --- a/src/generic/claims/mod.rs +++ b/src/generic/claims/mod.rs @@ -30,18 +30,18 @@ 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 chrono::prelude::*; use std::convert::TryFrom; + use time::format_description::well_known::Rfc3339; #[test] fn test_expiration_claim() -> Result<()> { // setup // a good time format - let now = Local::now(); - let s = now.to_rfc3339(); + let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; assert!(ExpirationClaim::try_from("hello").is_err()); - let claim = ExpirationClaim::try_from(s.as_str()); + let claim = ExpirationClaim::try_from(now); assert!(claim.is_ok()); let claim = claim.unwrap(); @@ -54,11 +54,10 @@ mod unit_tests { fn test_not_before_claim() -> Result<()> { // setup // a good time format - let now = Local::now(); - let s = now.to_rfc3339(); + let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; assert!(NotBeforeClaim::try_from("hello").is_err()); - let claim = NotBeforeClaim::try_from(s.as_str()); + let claim = NotBeforeClaim::try_from(now); assert!(claim.is_ok()); let claim = claim.unwrap(); @@ -71,11 +70,10 @@ mod unit_tests { fn test_issued_at_claim() -> Result<()> { // setup // a good time format - let now = Local::now(); - let s = now.to_rfc3339(); + let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; assert!(IssuedAtClaim::try_from("hello").is_err()); - let claim = IssuedAtClaim::try_from(s.as_str()); + let claim = IssuedAtClaim::try_from(now); assert!(claim.is_ok()); let claim = claim.unwrap(); diff --git a/src/lib.rs b/src/lib.rs index b387aed..d749607 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,8 @@ //! * Has no [implicit assertion](https://github.com/paseto-standard/paseto-spec/tree/master/docs) //! for V3 or V4 versioned tokens //! * Expires in **1 hour** after creation (due to an included default ExpirationClaim) -//! * Contains an IssuedAtClaim indicating when it was created +//! * Contains an IssuedAtClaim defaulting to the current utc time the token was created +//! * Contains a NotBeforeClaim defaulting to the current utc time the token was created //! //! //! You can parse and validate an existing token with the following: @@ -118,7 +119,7 @@ //! let token = PasetoBuilder::::default() //! .set_footer(Footer::from("Sometimes science is more art than science")) //! // note how we set the implicit assertion here -//! .set_implicit_assertion(ImplicitAssertion::from("Sometimes science is more art than science")) +//! .set_implicit_assertion(ImplicitAssertion::from("There’s a lesson here, and I’m not going to be the one to figure it out.")) //! .build(&key)?; //! //! // token is now a String in the form: "v4.local.encoded-payload.footer" @@ -131,12 +132,12 @@ //! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); //! # let token = PasetoBuilder::::default() //! # .set_footer(Footer::from("Sometimes science is more art than science")) -//! # .set_implicit_assertion(ImplicitAssertion::from("Sometimes science is more art than science")) +//! # .set_implicit_assertion(ImplicitAssertion::from("There’s a lesson here, and I’m not going to be the one to figure it out.")) //! # .build(&key)?; //! // now we can parse and validate the token with a parser that returns a serde_json::Value //! let json_value = PasetoParser::::default() //! .set_footer(Footer::from("Sometimes science is more art than science")) -//! .set_implicit_assertion(ImplicitAssertion::from("Sometimes science is more art than science")) +//! .set_implicit_assertion(ImplicitAssertion::from("There’s a lesson here, and I’m not going to be the one to figure it out.")) //! .parse(&token, &key)?; //! //! # //the ExpirationClaim @@ -155,12 +156,12 @@ //! PasetoClaimError>* //! ```rust //! # use rusty_paseto::prelude::*; -//! # use chrono::{Utc, Duration}; //! // must include //! use std::convert::TryFrom; //! let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); -//! // real-world example using the chrono crate to expire 5 minutes from now -//! let in_5_minutes = (Utc::now() + Duration::minutes(5)).to_rfc3339(); +//! // real-world example using the time crate to expire 5 minutes from now +//! # use time::format_description::well_known::Rfc3339; +//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?; //! //! let token = PasetoBuilder::::default() //! // note the TryFrom implmentation for ExpirationClaim @@ -171,7 +172,7 @@ //! //! // token is a String in the form: "v4.local.encoded-payload.footer" //! -//! # Ok::<(),GenericBuilderError>(()) +//! # Ok::<(),anyhow::Error>(()) //! ``` //! @@ -185,9 +186,9 @@ //! The method call required to do so ensures readers of the code understand the implicit risk. //! ```rust //! # use rusty_paseto::prelude::*; -//! # use chrono::{Utc, Duration}; +//! # use time::format_description::well_known::Rfc3339; //! # use std::convert::TryFrom; -//! # let in_5_minutes = (Utc::now() + Duration::minutes(5)).to_rfc3339(); +//! # let in_5_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(5)).format(&Rfc3339)?; //! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); //! let token = PasetoBuilder::::default() //! .set_claim(ExpirationClaim::try_from(in_5_minutes)?) @@ -196,7 +197,7 @@ //! .set_no_expiration_danger_acknowledged() //! .build(&key)?; //! -//! # Ok::<(),GenericBuilderError>(()) +//! # Ok::<(),anyhow::Error>(()) //! ``` //! //! ## Setting PASETO Claims @@ -204,15 +205,15 @@ //! The PASETO specification includes [seven reserved claims](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) which you can set with their explicit types: //! ```rust //! # use rusty_paseto::prelude::*; -//! # use chrono::{Utc, Duration}; +//! # 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 chrono crate to expire 5 minutes from now -//! # let in_5_minutes = (Utc::now() + Duration::minutes(5)).to_rfc3339(); -//! // real-world example using the chrono crate to prevent the token from being used before 2 +//! # // real-world example using the time crate to expire 5 minutes from now +//! # 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 -//! let in_2_minutes = (Utc::now() + Duration::minutes(2)).to_rfc3339(); +//! let in_2_minutes = (time::OffsetDateTime::now_utc() + time::Duration::minutes(2)).format(&Rfc3339)?; //! //! let token = PasetoBuilder::::default() //! //json payload key: "exp" @@ -234,7 +235,7 @@ //! .set_claim(TokenIdentifierClaim::from("Planet Music - Season 988")) //! .build(&key)?; //! -//! # Ok::<(),GenericBuilderError>(()) +//! # Ok::<(),anyhow::Error>(()) //! ``` //! //! ## Setting your own Custom Claims @@ -244,7 +245,6 @@ //! #### Note: *CustomClaims use the TryFrom trait and return a Result<(), PasetoClaimError> if you attempt to use one of the [reserved PASETO keys](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) in your CustomClaim* //! ```rust //! # use rusty_paseto::prelude::*; -//! # use chrono::{Utc, Duration}; //! # // must include //! # use std::convert::TryFrom; //! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); @@ -258,7 +258,6 @@ //! This throws an error: //! ```should_panic //! # use rusty_paseto::prelude::*; -//! # use chrono::{Utc, Duration}; //! # // must include //! # use std::convert::TryFrom; //! # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); diff --git a/src/prelude/error.rs b/src/prelude/error.rs new file mode 100644 index 0000000..3a90be5 --- /dev/null +++ b/src/prelude/error.rs @@ -0,0 +1,17 @@ +use thiserror::Error; + +/// Errors from validating claims in a parsed token +#[derive(Debug, Error)] +pub enum GeneralPasetoError { + ///A general, unspecified paseto error + #[error("A general paseto error occurred")] + PasetoError(Box), + #[error("An infallible error occurred")] + Infallible { + ///An infallible error + #[from] + source: std::convert::Infallible, + }, + #[error(transparent)] + RFC3339Date(#[from] time::error::Format), +} diff --git a/src/prelude/mod.rs b/src/prelude/mod.rs index 9bbf8ae..0fdcbbe 100644 --- a/src/prelude/mod.rs +++ b/src/prelude/mod.rs @@ -1,6 +1,8 @@ +mod error; mod paseto_builder; mod paseto_parser; pub use crate::generic::*; +pub use error::GeneralPasetoError; pub use paseto_builder::PasetoBuilder; pub use paseto_parser::PasetoParser; diff --git a/src/prelude/paseto_builder.rs b/src/prelude/paseto_builder.rs index a71bb97..5347f39 100644 --- a/src/prelude/paseto_builder.rs +++ b/src/prelude/paseto_builder.rs @@ -1,8 +1,8 @@ use crate::generic::*; -use chrono::{Duration, Utc}; use core::marker::PhantomData; use std::collections::HashSet; use std::convert::TryFrom; +use time::format_description::well_known::Rfc3339; pub struct PasetoBuilder<'a, Version, Purpose> { version: PhantomData, @@ -27,6 +27,11 @@ impl<'a, Version, Purpose> PasetoBuilder<'a, Version, Purpose> { pub fn set_claim(&mut self, value: T) -> &mut Self { //we need to inspect all the claims and verify there are no duplicates + //overwrite nbf default claim if provided + if value.get_key() == "nbf" { + //remove the existing claim + self.builder.remove_claim(value.get_key()); + } if !self.top_level_claims.insert(value.get_key().to_string()) { self.dup_top_level_found = (true, value.get_key().to_string()); } @@ -70,12 +75,21 @@ where impl<'a, Version, Purpose> Default for PasetoBuilder<'a, Version, Purpose> { fn default() -> Self { - let mut me = Self::new(); + //the unwraps in this function should be Infallible + let mut new_builder = Self::new(); + let now = time::OffsetDateTime::now_utc(); + let in_one_hour = now + time::Duration::hours(1); + + let expiration_time = in_one_hour.format(&Rfc3339).unwrap(); + let current_time = now.format(&Rfc3339).unwrap(); //set some defaults - me.builder - .set_claim(ExpirationClaim::try_from((Utc::now() + Duration::hours(1)).to_rfc3339()).unwrap()) - .set_claim(IssuedAtClaim::try_from(Utc::now().to_rfc3339()).unwrap()); - me + new_builder + .builder + .set_claim(ExpirationClaim::try_from(expiration_time).unwrap()) + .set_claim(IssuedAtClaim::try_from(current_time.clone()).unwrap()) + .set_claim(NotBeforeClaim::try_from(current_time).unwrap()); + + new_builder } } @@ -135,15 +149,17 @@ mod paseto_builder { use crate::prelude::*; use anyhow::Result; - use chrono::{Duration, Utc}; use std::convert::TryFrom; + use time::format_description::well_known::Rfc3339; #[test] fn duplicate_top_level_claim_test() -> Result<()> { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let tomorrow = (Utc::now() + Duration::days(1)).to_rfc3339(); + let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?; + + //let tomorrow = (Utc::now() + Duration::days(1)).to_rfc3339(); //create a builder, with default IssuedAtClaim let expected_error = format!( @@ -163,12 +179,33 @@ mod paseto_builder { Ok(()) } + #[test] + fn update_default_not_before_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)?; + + //create a builder, with default IssuedAtClaim + let token = PasetoBuilder::::default() + .set_claim(NotBeforeClaim::try_from(tomorrow).unwrap()) + .build(&key)?; + + //now let's decrypt the token and verify the values + //the IssuedAtClaim should exist and the date should be set to tomorrow + let token_error = PasetoParser::::default().parse(&token, &key).err().unwrap(); + + assert!(token_error.to_string().starts_with("The token cannot be used before ")); + + Ok(()) + } + #[test] fn update_default_issued_at_claim_test() -> Result<()> { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let tomorrow = (Utc::now() + Duration::days(1)).to_rfc3339(); + let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?; //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -185,12 +222,14 @@ 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 = Utc::now() + Duration::days(1); //the claimm should exist assert_eq!(key, "iat"); //date should be tomorrow - assert_eq!(datetime.date.to_string(), tomorrow.date().naive_utc().to_string()); + assert_eq!(datetime.date.to_string(), tomorrow); Ok(()) }) @@ -218,10 +257,11 @@ mod paseto_builder { let datetime = iso8601::datetime(val).unwrap(); + let now = time::OffsetDateTime::now_utc().date().to_string(); //the claimm should exist assert_eq!(key, "iat"); //date should be today - assert_eq!(datetime.date.to_string(), Utc::now().date().naive_utc().to_string()); + assert_eq!(datetime.date.to_string(), now); Ok(()) }) @@ -235,7 +275,8 @@ mod paseto_builder { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let in_4_days = (Utc::now() + Duration::days(4)).to_rfc3339(); + //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)?; //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -253,11 +294,14 @@ mod paseto_builder { 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 = Utc::now() + Duration::days(4); //the claimm should exist assert_eq!(key, "exp"); //date should be tomorrow - assert_eq!(datetime.date.to_string(), in_4_days.date().naive_utc().to_string()); + assert_eq!(datetime.date.to_string(), in_4_days); Ok(()) }) @@ -284,12 +328,15 @@ 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 tomorrow = Utc::now() + Duration::hours(1); + //let tomorrow = Utc::now() + Duration::hours(1); //the claimm should exist assert_eq!(key, "exp"); //date should be today - assert_eq!(datetime.date.to_string(), tomorrow.date().naive_utc().to_string()); + assert_eq!(datetime.date.to_string(), expires); Ok(()) }) diff --git a/src/prelude/paseto_parser.rs b/src/prelude/paseto_parser.rs index 3310ea5..1bb6adb 100644 --- a/src/prelude/paseto_parser.rs +++ b/src/prelude/paseto_parser.rs @@ -1,7 +1,7 @@ use crate::generic::*; -use chrono::{DateTime, Utc}; use core::marker::PhantomData; use serde_json::Value; +use time::format_description::well_known::Rfc3339; pub struct PasetoParser<'a, Version, Purpose> { version: PhantomData, @@ -62,9 +62,10 @@ impl<'a, Version, Purpose> Default for PasetoParser<'a, Version, Purpose> { return Ok(()); } //turn the value into a datetime - let datetime = DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + let datetime = + time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; //get the current datetime - let now = Utc::now(); + let now = time::OffsetDateTime::now_utc(); //here we do the actual validation check for the expiration claim if datetime <= now { @@ -83,9 +84,9 @@ impl<'a, Version, Purpose> Default for PasetoParser<'a, Version, Purpose> { //otherwise let's continue with the validation //turn the value into a datetime let not_before_time = - DateTime::parse_from_rfc3339(val).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; + time::OffsetDateTime::parse(val, &Rfc3339).map_err(|_| PasetoClaimError::RFC3339Date(val.to_string()))?; //get the current datetime - let now = Utc::now(); + let now = time::OffsetDateTime::now_utc(); //here we do the actual validation check for the expiration claim if now <= not_before_time { @@ -168,17 +169,18 @@ mod paseto_parser { use crate::prelude::*; use anyhow::Result; - use chrono::{Duration, Utc}; + use time::format_description::well_known::Rfc3339; #[test] fn usage_before_ready_test() -> Result<()> { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let not_before = Utc::now() + Duration::hours(1); + //let not_before = Utc::now() + Duration::hours(1); + let not_before = (time::OffsetDateTime::now_utc() + time::Duration::hours(1)).format(&Rfc3339)?; //create a default builder let token = PasetoBuilder::::default() - .set_claim(NotBeforeClaim::try_from(not_before.to_rfc3339())?) + .set_claim(NotBeforeClaim::try_from(not_before)?) .build(&key)?; let expected_error = format!( "{}", @@ -195,12 +197,12 @@ mod paseto_parser { let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); //we're going to set a token expiration date to 10 minutes ago - let expired = Utc::now() + Duration::minutes(-10); + let expired = (time::OffsetDateTime::now_utc() + time::Duration::minutes(-10)).format(&Rfc3339)?; //create a default builder let token = PasetoBuilder::::default() //setting our claim - .set_claim(ExpirationClaim::try_from(expired.to_rfc3339())?) + .set_claim(ExpirationClaim::try_from(expired)?) //by setting this we ensure we won't fail .set_no_expiration_danger_acknowledged() //without the line above this would have errored as an expired token @@ -219,10 +221,10 @@ mod paseto_parser { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let expired = Utc::now() + Duration::minutes(-10); + let expired = (time::OffsetDateTime::now_utc() + time::Duration::minutes(-10)).format(&Rfc3339)?; //create a default builder let token = PasetoBuilder::::default() - .set_claim(ExpirationClaim::try_from(expired.to_rfc3339())?) + .set_claim(ExpirationClaim::try_from(expired)?) .build(&key)?; let expected_error = format!( "{}", @@ -253,7 +255,7 @@ mod paseto_parser { //verify the default claims and no others are in the token assert!(json["exp"].is_string()); assert!(json["iat"].is_string()); - assert!(json["nbf"].is_null()); + assert!(json["nbf"].is_string()); assert!(json["sub"].is_null()); assert!(json["iss"].is_null()); assert!(json["jti"].is_null()); @@ -276,7 +278,7 @@ mod paseto_parser { //verify the default claims and no others are in the token assert!(json["exp"].is_string()); assert!(json["iat"].is_string()); - assert!(json["nbf"].is_null()); + assert!(json["nbf"].is_string()); assert!(json["sub"].is_null()); assert!(json["iss"].is_null()); assert!(json["jti"].is_null()); @@ -290,11 +292,11 @@ mod paseto_parser { //create a key let key = PasetoSymmetricKey::::from(Key::from(*b"wubbalubbadubdubwubbalubbadubdub")); - let tomorrow = (Utc::now() + Duration::days(1)).to_rfc3339(); + let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)).format(&Rfc3339)?; //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() - .set_claim(IssuedAtClaim::try_from(tomorrow.as_str()).unwrap()) + .set_claim(IssuedAtClaim::try_from(tomorrow).unwrap()) .build(&key)?; //now let's decrypt the token and verify the values @@ -306,11 +308,14 @@ mod paseto_parser { let datetime = iso8601::datetime(val).unwrap(); - let tomorrow = Utc::now() + Duration::days(1); + //let tomorrow = Utc::now() + Duration::days(1); + let tomorrow = (time::OffsetDateTime::now_utc() + time::Duration::days(1)) + .date() + .to_string(); //the claimm should exist assert_eq!(key, "iat"); //date should be tomorrow - assert_eq!(datetime.date.to_string(), tomorrow.date().naive_utc().to_string()); + assert_eq!(datetime.date.to_string(), tomorrow); Ok(()) }) @@ -337,9 +342,10 @@ mod paseto_parser { let datetime = iso8601::datetime(val).unwrap(); //the claimm should exist + let now = time::OffsetDateTime::now_utc().date().to_string(); assert_eq!(key, "iat"); //date should be today - assert_eq!(datetime.date.to_string(), Utc::now().date().naive_utc().to_string()); + assert_eq!(datetime.date.to_string(), now); Ok(()) }) @@ -353,7 +359,7 @@ mod paseto_parser { //create a key 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)?; //create a builder, with default IssuedAtClaim let token = PasetoBuilder::::default() @@ -369,11 +375,14 @@ mod paseto_parser { let datetime = iso8601::datetime(val).unwrap(); - let in_4_days = Utc::now() + Duration::days(4); + //let in_4_days = Utc::now() + Duration::days(4); + let in_4_days = (time::OffsetDateTime::now_utc() + time::Duration::days(4)) + .date() + .to_string(); //the claimm should exist assert_eq!(key, "exp"); //date should be tomorrow - assert_eq!(datetime.date.to_string(), in_4_days.date().naive_utc().to_string()); + assert_eq!(datetime.date.to_string(), in_4_days); Ok(()) }) @@ -399,11 +408,14 @@ mod paseto_parser { let datetime = iso8601::datetime(val).unwrap(); - let tomorrow = Utc::now() + Duration::hours(1); + let in_an_hour = (time::OffsetDateTime::now_utc() + time::Duration::hours(1)) + .time() + .hour() + .to_string(); //the claimm should exist assert_eq!(key, "exp"); //date should be today - assert_eq!(datetime.date.to_string(), tomorrow.date().naive_utc().to_string()); + assert_eq!(datetime.time.hour.to_string(), in_an_hour); Ok(()) })