Rust: JSON Web Token -- some investigative studies on crate jwt-simple.

behainguyen

Be Hai Nguyen

Posted on November 17, 2023

Rust: JSON Web Token -- some investigative studies on crate jwt-simple.

Via experimentations, I seek to answer the following question regarding crate jwt-simple: how do we check if a token is still valid, i.e. not already expired? I've found the answer to this question, and I hope it's a correct answer.

For the purpose of this post, we primarily use JSON Web Token (JWT) at the server side to verify the validity of an encoded string token before allowing access to certain resources. This encoded string token has originally been created by the server.

This Internet Engineering Task Force (IETF) RFC7519 article JSON Web Token (JWT) is the formal specification of JWT. This Wikipedia article JSON Web Token would make a somewhat easier reading on the subject.

Basically, a JWT token consists of three (3) parts: Header, Payload and Signature. The Signature is calculated from the Header and the Payload, and in a lot of implementations together with a secret string, defined by the applications which use the library.

All components are encoded using Base 64 Url Encoding strings. The period or full stop, i.e. ., separates each part.

An example of token from RFC7519 section 3.1, where line breaks are for display purposes only:

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Enter fullscreen mode Exit fullscreen mode

🚀 If we paste our own JWT tokens into https://jwt.io/, it should be decoded correctly, and we should see the information in Payload displayed as a JSON object. However, all tokens generated throughout this post always get reported Invalid Signature.

This Stack Overflow post discusses this issue -- this error does not seem at all relevant to our discussion.

The Payload has a set of standard claims. The specification defines these Registered Claim Names. Some are required, that is, if the Payload does not have any of these, the final token is not valid. We can define our own custom claims in the Payload, too; e.g., an email field.

There's a plethora of crates which implement JWT. I've studied two (2). Crate jwt-simple has more than 1 (one) million downloads and seems to be being actively maintained. However, I've yet to find any post which comprehensively discusses how to use it. And after spending times studying it, I feel that its documentations could improve significantly.

One of the central tenets of JWT is checking for token expiration. Reading through the official examples, it isn't clear how we go about doing that. After some experimentations, I seem to get it, at least, I think it'll work for me. Let's go through the experimentations which I've carried out.

Cargo.toml is common for all examples. Its dependencies section is as follow:

...
[dependencies]
jwt-simple = "0.11"
serde = {version = "1.0.188", features = ["derive"]}
Enter fullscreen mode Exit fullscreen mode

Please note: the example code has been tested on both Windows 10 and Ubuntu 22.10.

Also note that this crate does not use a secret string to calculate token's Signature.

❶ In this first example, which is taken from the official documentation, the simplest example, we just create a token with no custom Payload, then decode the token, we also print out relevant data for visual inspection.

Content of src/main.rs:
Enter fullscreen mode Exit fullscreen mode
use jwt_simple::prelude::*;

fn main() {
    // create a new key for the `HS256` JWT algorithm
    let key = HS256Key::generate();

    // create claims valid for 2 hours
    let claims = Claims::create(Duration::from_hours(2));

    println!("1. claims: {:#?}", claims);

    let token = match key.authenticate(claims) {
        Ok(x) => x,
        Err(e) => {
            println!("Failed to create token. Error: {}", e.to_string());
            panic!("Program terminated!");
        }
    };

    println!("1. token: {:#?}", token);

    let claims1 = match key.verify_token::<NoCustomClaims>(&token, None) {
        Ok(x) => x,
        Err(e) => {
            println!("Failed to verify token. Error: {}", e.to_string());
            panic!("Program terminated!");
        }        
    };

    println!("2. claims: {:#?}", claims1);
}
Enter fullscreen mode Exit fullscreen mode

Output:

1. claims: JWTClaims {
    issued_at: Some(
        Duration(
            7301655513801696707,
        ),
    ),
    expires_at: Some(
        Duration(
            7301686437566227907,
        ),
    ),
    invalid_before: Some(
        Duration(
            7301655513801696707,
        ),
    ),
    issuer: None,
    subject: None,
    audiences: None,
    jwt_id: None,
    nonce: None,
    custom: NoCustomClaims,
}
1. token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDAwNDkxNTMsImV4cCI6MTcwMDA1NjM1MywibmJmIjoxNzAwMDQ5MTUzfQ.ZF4SIJJ-PhSyL6cm_ocHuStZ5tv4hdYK-kg9RZ8kT-Y"
2. claims: JWTClaims {
    issued_at: Some(
        Duration(
            7301655513727500288,
        ),
    ),
    expires_at: Some(
        Duration(
            7301686437492031488,
        ),
    ),
    invalid_before: Some(
        Duration(
            7301655513727500288,
        ),
    ),
    issuer: None,
    subject: None,
    audiences: None,
    jwt_id: None,
    nonce: None,
    custom: NoCustomClaims,
}
Enter fullscreen mode Exit fullscreen mode

Looking at the output, the following issues jump out:

Struct jwt_simple::claims::JWTClaims does not use any of the Registered Claim Names.

⓶ For the original claim (1. claims:), the value of issued_at is 7301655513801696707, and for the decoded or verified claim, (2. claims:), the value of issued_at is 7301655513727500288: they are different. Values for expires_at across both claims are also different. I started out with the assumption that values for each should be the same in both claims! I don't know what this means.

⓷ Values of issued_at, expires_at are large. The documentation does not tell us what they represent! Struct jwt_simple::claims::JWTClaims shows that they're type alias jwt_simple::prelude::UnixTimeStamp, and the real data type is struct jwt_simple::prelude::Duration; whom documentation states:

A duration type to represent an approximate span of time

We've made this call Duration::from_hours(2) in the code. And the documentation for pub fn from_hours(hours: u64) -> Duration states:

Creates a new Duration from the specified number of hours

It is not clear what they actually are. We're coming back to this question in ❸ example three in a section following.

❷ In this second example, we're looking at custom claim or custom Payload. The code is taken from the following sections of the official documentation: Key pairs and tokens creation and Custom claims. However, I define my own Payload rather than using struct MyAdditionalData from the document.

Content of src/main.rs:
Enter fullscreen mode Exit fullscreen mode
use jwt_simple::prelude::*;

// A custom payload.
#[derive(Serialize, Deserialize, Debug)]
pub struct PayLoad {
    iat: u64,
    exp: u64,
    email: String,
}

fn main() {
    // create a new key pair for the `ES256` JWT algorithm
    let key_pair = ES256KeyPair::generate();

    // a public key can be extracted from a key pair:
    let public_key = key_pair.public_key();

    // iat, exp are willy-nilly set to some random values.
    let pay_load = PayLoad {
        iat: 1,
        exp: 5,
        email: String::from("behai_nguyen@hotmail.com"),
    };

    // Claim creation with custom data:
    let claims = Claims::with_custom_claims(pay_load, Duration::from_secs(30));

    println!("1. claims: {:#?}", claims);

    let token = key_pair.sign(claims).expect("Failed to sign claims.");

    println!("1. token: {:#?}", token);

    // Claim verification with custom data. Note the presence of the custom data type:

    let claims1 = match public_key.verify_token::<PayLoad>(&token, None) {
        Ok(x) => x,
        Err(e) => {
            println!("Failed to verify token. Error: {}", e.to_string());
            panic!("Program terminated!");
        }        
    };

    println!("2. claims: {:#?}", claims1);

    assert_eq!(claims1.custom.email, "behai_nguyen@hotmail.com");
}
Enter fullscreen mode Exit fullscreen mode

In pub struct PayLoad, I purposely use the standard names for fields iat and exp.

Token creation happens successfully, but verifying or decoding the token results in the following handled error:

Failed to verify token. Error: duplicate field iat at line 1 column 57

Commented out iat proving that exp also results in a pretty much similar error.

❸ It'd seem that we should use expires_at to check for token expiration. We come back to the question above: what are the values of issued_at and expires_at?

In other words, when the server receives the token, what value do we have to work out to compare against expires_at, to determine if the token has expired or not?

The source code, lines 303-331 show how values for issued_at and expires_at are calculated:

...
pub struct Claims;

impl Claims {
    /// Create a new set of claims, without custom data, expiring in
    /// `valid_for`.
    pub fn create(valid_for: Duration) -> JWTClaims<NoCustomClaims> {
        let now = Some(Clock::now_since_epoch());
        JWTClaims {
            issued_at: now,
            expires_at: Some(now.unwrap() + valid_for),         
...
    }

    /// Create a new set of claims, with custom data, expiring in `valid_for`.
    pub fn with_custom_claims<CustomClaims: Serialize + DeserializeOwned>(
        custom_claims: CustomClaims,
        valid_for: Duration,
    ) -> JWTClaims<CustomClaims> {
        let now = Some(Clock::now_since_epoch());
        JWTClaims {
            issued_at: now,
            expires_at: Some(now.unwrap() + valid_for),
...         
Enter fullscreen mode Exit fullscreen mode

The value for issued_at is Some(Clock::now_since_epoch()).

The documentation for pub fn now_since_epoch() -> Duration states:

Returns the elapsed time since the UNIX epoch

-- It'd be nice if they also state what the unit of this elapsed time since the UNIX epoch is!

🦀 Please note, standard time std::time defines Constant std::time::UNIX_EPOCH, the “seconds since epoch” is calculated based on this constant, see this example.

This large Struct jwt_simple::prelude::Duration number is actually the number of “ticks”, obtained by calling pub fn as_ticks(&self) -> u64.

Let's try to mimic issued_at and expires_at calculations as per by the crate.

Content of src/main.rs:
Enter fullscreen mode Exit fullscreen mode
use jwt_simple::prelude::*;

fn main() {
    let issued_at = Clock::now_since_epoch();
    let valid_for = Duration::from_hours(1);
    let expires_at = issued_at + valid_for;

    println!("issued_at: {:#?}", issued_at);
    println!("issued_at.as_ticks(): {}", issued_at.as_ticks());

    println!("expires_at: {:#?}", expires_at);
    println!("expires_at.as_ticks(): {}", expires_at.as_ticks());    
}
Enter fullscreen mode Exit fullscreen mode

Output:

issued_at: Duration(
    7302219092803468401,
)
issued_at.as_ticks(): 7302219092803468401
expires_at: Duration(
    7302234554685734001,
)
expires_at.as_ticks(): 7302234554685734001
Enter fullscreen mode Exit fullscreen mode

❹ In this final example, we're attempting to do expiration check based on expires_at:

Create a token with a custom Payload (although not used in the code). This token is valid for only a few short seconds, defined by constant SECONDS_VALID_FOR. Then sleep for a few short seconds, defined by constant SECONDS_TO_SLEEP. Then decode the token. Next, obtain the current time using pub fn now_since_epoch() -> Duration, as per ❸ example three above. Finally, we compare expires_at and current time using ticks, seconds should also work, if expires_at is greater than the current time, the token is still valid, otherwise it is expired.

Content of src/main.rs:
Enter fullscreen mode Exit fullscreen mode
use std;
use jwt_simple::prelude::*;

static SECONDS_VALID_FOR: u64 = 5;
static SECONDS_TO_SLEEP: u64 = 4;

// A custom payload.
#[derive(Serialize, Deserialize, Debug)]
pub struct PayLoad {
    email: String,
}

fn as_ticks(val: Option<Duration>) -> u64 {
    // This call will never fail!
    match val {
        Some(x) => x.as_ticks(),
        None => unreachable!(), // ! Coerced to u64.
    }
}

fn main() {
    // create a new key pair for the `ES256` JWT algorithm
    let key_pair = ES256KeyPair::generate();

    // a public key can be extracted from a key pair:
    let public_key = key_pair.public_key();

    let pay_load = PayLoad {
        email: String::from("behai_nguyen@hotmail.com"),
    };

    // Create a token and send it to the client.
    let claims1 = Claims::with_custom_claims(pay_load, Duration::from_secs(SECONDS_VALID_FOR));
    let token1 = key_pair.sign(claims1).expect("1. Failed to sign claims.");

    // Sleep for SECONDS_TO_SLEEP seconds.
    // (I.e. a few seconds later, the client submits the token to the server.)
    let sleep_time = std::time::Duration::from_secs(SECONDS_TO_SLEEP);
    std::thread::sleep(sleep_time);

    // Token has been submitted to the server: verify it.
    // Assume not failing!
    let verified_claims1 = public_key.verify_token::<PayLoad>(&token1, None)
        .expect("1. Failed to verify token.");

    // Getting current time.
    let now = Some(Clock::now_since_epoch());

    if as_ticks(verified_claims1.expires_at) > as_ticks(now) {
        println!("Token is still valid!");
    }
    else {
        println!("Token expired already!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Adjust the values of SECONDS_VALID_FOR and SECONDS_TO_SLEEP, recompile, and see the response. ⓵ When SECONDS_VALID_FOR is greater than SECONDS_TO_SLEEP, we should get “Token is still valid!”. ⓶ When SECONDS_VALID_FOR is equal to or less than SECONDS_TO_SLEEP, we should get “Token expired already!

I hope my interpretation of how this jwt-simple crate works is correct. Before finishing up this post, I'd like to point out the followings:

  • Unanswered question: why do issued_at and expires_at values are different when a token is decoded (verified). I really don't get this behaviour.
  • The documentation mentions that we could write the generated key to file as bytes, then later read it back and use it. I've tried this, and it works. I'm not yet certain how to apply this to a proper web application.
  • And finally, in the next post, I'm doing a similar sort of studies on another JWT crate.

I do hope I haven't made any mistake in this post, and you find the information in this post helpful. Thank you for reading and stay safe as always.

✿✿✿

Feature image source:

💖 💪 🙅 🚩
behainguyen
Be Hai Nguyen

Posted on November 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related