Skip to content

Commit

Permalink
Swap chronos for time crate
Browse files Browse the repository at this point in the history
  • Loading branch information
rrrodzilla committed Dec 11, 2021
1 parent 160377a commit cb9a39a
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 72 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rusty_paseto"
version = "0.2.3"
version = "0.2.4"
edition = "2021"
readme = "readme.md"
authors = ["Roland Rodriguez <rolandrodriguez@gmail.com"]
Expand All @@ -26,11 +26,11 @@ serde_json = { version = "^1.0.68"}
thiserror = "1.0.29"
iso8601 = "0.4.0"
erased-serde = "0.3.16"
chrono = "0.4.19"
aes = {version = "0.7.5", features = ["ctr"]}
hmac = "0.11.0"
sha2 = "0.9.8"
zeroize = {version = "1.4.3", features = ["zeroize_derive"]}
time = {version = "0.3.5", features = ["parsing", "formatting"]}

[dev-dependencies]
anyhow = "1.0.45"
Expand Down
286 changes: 286 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,289 @@ Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of the
| [PASETO Test vectors](https://github.com/paseto-standard/test-vectors) | :green_circle: | :green_circle: | :green_circle: | :green_circle: | :green_circle: | :black_circle: | :green_circle: | :green_circle: |
| Documentation | :black_circle: | :black_circle: | :orange_circle: | :black_circle: | :black_circle: | :black_circle: | :black_circle: | :black_circle: |

| Feature | Status |
| ------------: | :-----------: |
| Feature gates | :black_circle: |
| PASERK suppor | :black_circle: |


# Usage

```Rust
// at the top of your source file
use rusty_paseto::prelude::*;
```
# Examples: Building and parsing tokens

Here's a basic, default token:
```Rust
use rusty_paseto::prelude::*;

// create a key specifying the PASETO version and purpose
let key = PasetoSymmetricKey::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
// use a default token builder with the same PASETO version and purpose
let token = PasetoBuilder::<V4, Local>::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::<V4, Local>::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::<V4, Local>::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::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
let token = PasetoBuilder::<V4, Local>::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::<V4, Local>::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::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
let token = PasetoBuilder::<V4, Local>::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::<V4, Local>::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::<V4, Local>::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub"));
// real-world example using the time crate to expire 5 minutes from now

let token = PasetoBuilder::<V4, Local>::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::<V4, Local>::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::<V4, Local>::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::<V4, Local>::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::<V4, Local>::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::<V4, Local>::default()
.set_claim(SubjectClaim::from("Get schwifty"))
.set_claim(CustomClaim::try_from(("Contestant", "Earth"))?)
.set_claim(CustomClaim::try_from(("Universe", 137))?)
.build(&key)?;

PasetoParser::<V4, Local>::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::<V4, Local>::default()
.set_claim(SubjectClaim::from("Get schwifty"))
.set_claim(CustomClaim::try_from(("Contestant", "Earth"))?)
.set_claim(CustomClaim::try_from(("Universe", 137))?)
.build(&key)?;

PasetoParser::<V4, Local>::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::<V4, Local>::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)

18 changes: 8 additions & 10 deletions src/generic/claims/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();

Expand All @@ -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();

Expand Down
Loading

0 comments on commit cb9a39a

Please sign in to comment.