#jwt #recursion #token #scope #oauth-token #oauth

rjwt

A recursive Javascript Web Token with support for ECDSA and Falcon

6 releases (breaking)

0.5.0 Jan 26, 2024
0.4.3 Jan 15, 2023
0.4.1 Feb 12, 2021
0.3.1 Feb 11, 2021
0.1.0 Feb 10, 2021

#253 in Authentication

Download history 13/week @ 2023-12-20 26/week @ 2024-01-24 5/week @ 2024-01-31 3/week @ 2024-02-14 22/week @ 2024-02-21 15/week @ 2024-02-28 4/week @ 2024-03-06 23/week @ 2024-03-13 10/week @ 2024-03-27 23/week @ 2024-04-03

58 downloads per month
Used in 10 crates (3 directly)

Apache-2.0

25KB
490 lines

Provides an Actor and (de)serializable Token struct which support authenticating JSON Web Tokens with a custom payload. See jwt.io for more information on the JWT spec.

The provided Actor uses the ECDSA algorithm to sign tokens (using the ed25519_dalek crate).

This library differs from other JWT implementations in that it allows for recursive Tokens.

Note that if the same (host, actor) pair is specified multiple times in the token chain, only the latest is returned by Claims::get.

Example:

use rjwt::*;

#[derive(Clone)]
struct Resolver {
    hostname: String,
    actors: HashMap<String, Actor<String>>,
    peers: Vec<Self>,
}
// ...

#[async_trait]
impl Resolve for Resolver {
    type HostId = String;
    type ActorId = String;
    type Claims = String;

    async fn resolve(&self, host: &Self::HostId, actor_id: &Self::ActorId) -> Result<Actor<Self::ActorId>, Error> {
        if host == &self.hostname {
            self.actors.get(actor_id).cloned().ok_or_else(|| Error::fetch(actor_id))
        } else if let Some(peer) = self.peers.iter().filter(|p| &p.hostname == host).next() {
            peer.resolve(host, actor_id).await
        } else {
            Err(Error::fetch(host))
        }
    }
}

let now = SystemTime::now();

// Say that Bob is a user on example.com.
let bobs_id = "bob".to_string();
let example_dot_com = "example.com".to_string();

let actor_bob = Actor::new(bobs_id.clone());
let example = Resolver::new(example_dot_com.clone(), [actor_bob.clone()], vec![]);

// Bob makes a request through the retailer.com app.
let retailer_dot_com = "retailer.com".to_string();
let retail_app = Actor::new("app".to_string());
let retailer = Resolver::new(
    retailer_dot_com.clone(),
    [retail_app.clone()],
    vec![example.clone()]);

// The retailer.com app makes a request to Bob's bank.
let bank_account = Actor::new("bank".to_string());
let bank = Resolver::new(
    "bank.com".to_string(),
    [bank_account.clone()],
    vec![example, retailer.clone()]);

// First, example.com issues a token to authenticate Bob.
let bobs_claim = String::from("I am Bob and retailer.com may debit my bank.com account");

// This requires constructing the token...
let bobs_token = Token::new(
    example_dot_com.clone(),
    now,
    Duration::from_secs(30),
    actor_bob.id().to_string(),
    bobs_claim.clone());

// and signing it with Bob's private key.
let bobs_token = actor_bob.sign_token(bobs_token).expect("signed token");

// Then, retailer.com validates the token...
let bobs_token = block_on(retailer.verify(bobs_token.into_jwt(), now)).expect("claims");
assert!(bobs_token.get_claim(&example_dot_com, &bobs_id).expect("claim").starts_with("I am Bob"));

// and adds its own claim, that Bob owes it $1.
let retailer_claim = String::from("Bob spent $1 on retailer.com");
let retailer_token = retail_app.consume_and_sign(
    bobs_token,
    retailer_dot_com.clone(),
    retailer_claim.clone(),
    now).expect("signed token");

assert_eq!(retailer_token.get_claim(&retailer_dot_com, retail_app.id()), Some(&retailer_claim));
assert_eq!(retailer_token.get_claim(&example_dot_com, actor_bob.id()), Some(&bobs_claim));

// Finally, Bob's bank verifies the token...
let retailer_token_as_received = block_on(bank.verify(retailer_token.jwt().to_string(), now)).expect("claims");
assert_eq!(retailer_token, retailer_token_as_received);

// to authenticate that the request came from Bob...
assert!(retailer_token_as_received
    .get_claim(&example_dot_com, &bobs_id)
    .expect("claim")
    .starts_with("I am Bob and retailer.com may debit my bank.com account"));

// via retailer.com.
assert!(retailer_token_as_received.get_claim(&retailer_dot_com, retail_app.id()).expect("claim").starts_with("Bob spent $1"));

Dependencies

~3.5–5MB
~107K SLoC