Rust: JSON Web Token -- some investigative studies on crate jsonwebtoken.
Tags: rust, jwt, jsonwebtoken
Published: 2023-11-20 00:03:54

<em>Regarding crate <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/index.html" title="jsonwebtoken" target="_blank">jsonwebtoken</a>, the primary question is still <em>how to check if a token is still valid, i.e. not already expired?</em> We're discussing this question in this post.</em>

| ![090-feature-image.png](https://behainguyen.files.wordpress.com/2023/11/090-feature-image.png) |
|:--:|
| *Rust: JSON Web Token -- some investigative studies on crate jsonwebtoken.* |

We've previously done <a href="https://behainguyen.wordpress.com/2023/11/17/rust-json-web-token-some-investigative-studies-on-crate-jwt-simple/" title="Rust: JSON Web Token -- some investigative studies on crate jwt-simple." target="_blank">some studies on crate jwt-simple</a>. And we've reviewed some background information on <a href="https://behainguyen.wordpress.com/2023/11/17/rust-json-web-token-some-investigative-studies-on-crate-jwt-simple#jwt-background" title="JSON Web Token (JWT)" target="_blank">JSON Web Token (JWT)</a>.

In this post, we're studying crate <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/index.html" title="jsonwebtoken" target="_blank">jsonwebtoken</a>. This crate has more than twelve (12) million downloads, and also seems to be actively maintained.

Crate <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/index.html" title="jsonwebtoken" target="_blank">jsonwebtoken</a> also appears to be simpler to use than <a href="https://docs.rs/jwt-simple/latest/jwt_simple/" title="jwt-simple" target="_blank">jwt-simple</a>. It doesn't seem to assume much default, I feel having much more control over the usage of this crate.

-- For me, personally, I feel that it's closer to a session expiration management approach which I've coded before.

Base on the documentation, it employs both <code>secret string</code> and key pair to calculate token's <code>Signature</code>. We're only using <code>secret string</code> in this post.

🚀 As per the <a href="https://behainguyen.wordpress.com/2023/11/17/rust-json-web-token-some-investigative-studies-on-crate-jwt-simple/" title="Rust: JSON Web Token -- some investigative studies on crate jwt-simple." target="_blank">previous post on jwt-simple</a>, tokens generated by the examples in this post also suffer <span style="color:red;"><strong><code>Invalid Signature</code></strong></span> when pasted into <a href="https://jwt.io/" title="jwt.io | Auth0 by Okta" target="_blank">https://jwt.io/</a>. We can safely ignore this report.

-- <strong><em>Again, the primary objective is to find out how to manage token expiration.</em></strong>

<code>Cargo.toml</code> is common for all examples. Its <code>dependencies</code> section is as follow:


```toml
...
[dependencies]
time = { version = "0.3.22", default-features = false, features = ["formatting", "macros", "parsing"] }
serde = { version = "1.0.188", features = ["derive"] }
jsonwebtoken = "9.1"
```

<strong>Please note:</strong> <em>the example code has been tested on both Windows 10 and Ubuntu 22.10.</em>

<a id="the-first-example">❶ In this first example</a>, we'll just combine examples from the following two (2) sections <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/fn.encode.html" title="Function jsonwebtoken::encode" target="_blank">Function jsonwebtoken::encode</a> and <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/fn.decode.html" title="Function jsonwebtoken::decode" target="_blank">Function jsonwebtoken::decode</a> from the official documentation.

```
Content of src/main.rs:
```

```rust
use serde::{Deserialize, Serialize};
use jsonwebtoken::{encode, Algorithm, Header, EncodingKey};
use jsonwebtoken::{decode, DecodingKey, Validation};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
   sub: String,
   company: String
}

fn main() {
    let my_claims = Claims {
        sub: "b@b.com".to_owned(),
        company: "ACME".to_owned()
    };

    // my_claims is a struct that implements Serialize
    // This will create a JWT using HS256 as algorithm
    let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref())).unwrap();

    // Claims is a struct that implements Deserialize
    let token_message = decode::<Claims>(&token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256));

    println!("token_message: {:#?}", token_message);
}
```

It's simple, create a token using a default <code>Header</code> and a custom <code>Payload</code>. The <code>Signature</code> is calculated using a <code>secret string</code>.

But it results in an error:

```
token_message: Err(
    Error(
        MissingRequiredClaim(
            "exp",
        ),
    ),
)
```

Adding required field <code>exp</code> to <code>struct Claims</code> fixes the above error:

```rust
...
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
   sub: String,
   company: String,
   exp: u64,
}
...
    let my_claims = Claims {
        sub: "b@b.com".to_owned(),
        company: "ACME".to_owned(),
        exp: 10000000000, // Just some random number.
    };
...	
```

And the output is:

```
token_message: Ok(
    TokenData {
        header: Header {
            typ: Some(
                "JWT",
            ),
            alg: HS256,
            cty: None,
            jku: None,
            jwk: None,
            kid: None,
            x5u: None,
            x5c: None,
            x5t: None,
            x5t_s256: None,
        },
        claims: Claims {
            sub: "b@b.com",
            company: "ACME",
            exp: 10000000000,
        },
    },
)
```

<a id="struct-validation"></a>🦀 Also, in the decode token call, take note of the last parameter <code>&Validation::new(Algorithm::HS256)</code>. This's its documentation page <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/struct.Validation.html" title="Struct jsonwebtoken::Validation" target="_blank">Struct jsonwebtoken::Validation</a>. We're interested in the field <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/struct.Validation.html#structfield.validate_exp" title="validate_exp: bool" target="_blank">validate_exp: bool</a>, which by default has been set to validate if a token has been expired: i.e., if it has, the decode token call would return an appropriate <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/errors/enum.ErrorKind.html" title="Enum jsonwebtoken::errors::ErrorKind" target="_blank">Enum jsonwebtoken::errors::ErrorKind</a> value, which we're using in the next example.

The GitHub repo's <a href="https://github.com/Keats/jsonwebtoken/tree/master/examples" title="GitHub examples" target="_blank">examples directory</a> has a few more examples. Except for <code>auth0.rs</code> which results in a run time error, every others work.

<a id="the-second-example">❷ In this second example</a>, and also the final one, we are going to simulate server-client token exchanging and expiration checking. The example is a bit long, but it's only a sequence of steps, no tricky logic. 

We'll implement the standard <code>iat</code> field, the value of this field gets set once, and stays fixed during the entire life of a token. It's more or less for historical purposes. For expiration management, the standard <code>exp</code> field is used; every time the server receives a token from a client, if the token is still valid, the value of this field gets updated to a new expiry time.

Initially, when the token first created by the server, <code>iat</code> gets set to “seconds since epoch”, which we've discussed in <a href="https://behainguyen.wordpress.com/2023/11/13/rust-seconds-since-epoch-1970-01-01-000000-utc/" title="Rust: seconds since epoch -- “1970-01-01 00:00:00 UTC”." target="_blank">Rust: seconds since epoch -- “1970-01-01 00:00:00 UTC”</a>. And <code>exp</code> is <code>iat</code> plus (+) the duration in which the token is valid for, which's the value of the constant <code>SECONDS_VALID_FOR</code>.

In a similar manner, on subsequent updates, <code>exp</code> gets set to the current “seconds since epoch” plus (+) the duration expressed as seconds by constant <code>SECONDS_VALID_FOR</code>.

🚀 <strong>It should be obvious that: <em>this implementation implies <code>SECONDS_VALID_FOR</code> is the duration the token stays valid since last active</em></strong>. It does not mean that after this duration, the token becomes invalid or expired. So long as the client keeps sending requests while the token is valid, it will never expire!

Let's see the example, then we'll briefly discuss it.

In the code, please pay attention to the instance <code>validation</code> of <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/struct.Validation.html" title="Struct jsonwebtoken::Validation" target="_blank">Struct jsonwebtoken::Validation</a>, which we've mentioned <a href="#struct-validation">previously</a>. We set <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/struct.Validation.html#structfield.leeway" title="leeway" target="_blank">leeway</a> field to <code>0</code> to make it simpler for the purpose of studying token expiration behaviour.

```
Content of src/main.rs:
```

```rust
use std;
use serde::{Deserialize, Serialize};
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{encode, get_current_timestamp, Algorithm, Header, EncodingKey};
use jsonwebtoken::{decode, DecodingKey, Validation};

// A custom payload.
#[derive(Serialize, Deserialize, Debug)]
pub struct PayLoad {
    iat: u64, // This standard field value stays fixed.
    exp: u64, // Required.
    email: String,
}

//
// When adjust, ensure that: ( SECONDS_TO_SLEEP * 2 ) > SECONDS_VALID_FOR.
//
static SECONDS_VALID_FOR: u64 = 5;
static SECONDS_TO_SLEEP: u64 = 4;

static SECRET_KEY: &str = "007: The Spy Who Loved Me";

fn seconds_since_epoch() -> u64 {
    get_current_timestamp()
}

fn expiry(secs_since_epoch: u64, secs_valid_for: u64) -> u64 {
    secs_since_epoch + secs_valid_for
}

fn main() {
    //
    // Server prepares a brand new token to be sent to a client.
    //

    let iat = seconds_since_epoch();
    let exp = expiry(iat, SECONDS_VALID_FOR);

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

    println!("original iat: {}", pay_load.iat);
    println!("original exp: {}\n", pay_load.exp);

    // Panic when in error, in proper code, it should return an error.
    let token1 = match encode(&Header::default(), 
        &pay_load, 
        &EncodingKey::from_secret(SECRET_KEY.as_ref())) {
            Ok(x) => x,

            Err(e) => {
                println!("1. Failed to encode token. Error: {}", e.to_string());
                panic!("1. Program terminated!");
            }
    };

    //
    // Default global validation.
    //
    let mut validation = Validation::new(Algorithm::HS256);
    //
    // For the shake of simplicity, 0 would make leeway not having any effect
    // on expiration calculations.
    //
    validation.leeway = 0;

    //
    // Simulate token has not been expired.
    //
    // After SECONDS_TO_SLEEP seconds the client submits the token 
    // to the server to request some resource.
    //

    // After SECONDS_TO_SLEEP seconds...
    let sleep_time = std::time::Duration::from_secs(SECONDS_TO_SLEEP);
    std::thread::sleep(sleep_time);

    let mut ret_claims: PayLoad;
    // Server decodes the token.
    // Panic when in error, in proper code, it should return an error.    
    match decode::<PayLoad>(&token1, 
        &DecodingKey::from_secret(SECRET_KEY.as_ref()), &validation) {
            Ok(x) => ret_claims = x.claims,

            Err(err) => match *err.kind() {
                ErrorKind::InvalidToken => panic!("1. Decode: token is invalid"),
                ErrorKind::ExpiredSignature => panic!("1. Decode: token has expired"),
                _ => panic!("1. Decode: some other errors"),
            },
    };

    println!("ret_claims.iat: {}", ret_claims.iat);
    println!("ret_claims.exp before updated: {}\n", ret_claims.exp);

    // Get the current time.
    let secs_since_epoch = seconds_since_epoch();

    println!("1. current time: {}\n", secs_since_epoch);

    // If the token has not been expired, server the request resource.
    // At this step, the code as it is, the token has not been expired.
    println!("1. Token is still valid!\n");

    //
    // The token has been verified valid.
    // Update the payload's exp value, then prepare an updated token and 
    // send back to the client.
    //

    // Expiry should be moved back by SECONDS_VALID_FOR seconds from current time.
    ret_claims.exp = expiry(secs_since_epoch, SECONDS_VALID_FOR);

    println!("ret_claims.exp after updated: {}\n", ret_claims.exp);    

    //
    // Updated token sent back to the client.
    //
    let token2 = encode(&Header::default(), 
        &ret_claims, &EncodingKey::from_secret(SECRET_KEY.as_ref())).unwrap();

    //
    // Simulate token has been expired.
    //
    // After ( SECONDS_TO_SLEEP * 2 ) seconds the client submits the token 
    // to the server to request some resource.
    //

    // After SECONDS_TO_SLEEP seconds...
    let sleep_time = std::time::Duration::from_secs(SECONDS_TO_SLEEP * 2);
    std::thread::sleep(sleep_time);

    let ret_claims2: PayLoad;

    // Server decodes the token.
    // Panic when in error, in proper code, it should return an error.    
    match decode::<PayLoad>(&token2, 
        &DecodingKey::from_secret(SECRET_KEY.as_ref()), &validation) {
            Ok(x) => ret_claims2 = x.claims,

            Err(err) => match *err.kind() {
                ErrorKind::InvalidToken => panic!("2. Decode: token is invalid"),
                ErrorKind::ExpiredSignature => panic!("2. Decode: token has expired"),
                _ => panic!("2. Decode: some other errors"),
            }, 
    };

    println!("ret_claims2.iat: {}", ret_claims2.iat);
    println!("ret_claims2.exp: {}\n", ret_claims2.exp);

    // Get the current time.
    let secs_since_epoch = seconds_since_epoch();

    println!("2. current time: {}\n", secs_since_epoch);

    // If the token has not been expired, server the request resource.
    // At this step, the code as it is, the token has been expired: this message
    // never gets printed out.
    println!("2. Token is still valid!");
}
```

The example has the following sequential steps. ⓵ The server creates a <code>token1</code>, (and of course sends it the client.) ⓶ Waits for <code>SECONDS_TO_SLEEP</code> seconds before receiving the token back from the client. ⓷ The server should decode the token successfully, it shouldn't be expired. ⓸ Then the server updates the value of the <code>exp</code> field of the decoded token. ⓹ From the updated claim, it prepares a new <code>token2</code> and sends this to the client. ⓺ Waits for <code>(SECONDS_TO_SLEEP * 2)</code> seconds before receiving the token back from the client. ⓻ Decoding this <code>token2</code> should result in error <a href="https://docs.rs/jsonwebtoken/latest/jsonwebtoken/errors/enum.ErrorKind.html#variant.ExpiredSignature" title="ExpiredSignature" target="_blank">ExpiredSignature</a>, which cause <code>panic!("2. Decode: token has expired")</code> to come into effect. 

Please also note that, along each step, we also print out some information for visual inspection.

The example code unaltered as is, its output's as follows:

```
original iat: 1700390661
original exp: 1700390666

ret_claims.iat: 1700390661
ret_claims.exp before updated: 1700390666

1. current time: 1700390665

1. Token is still valid!

ret_claims.exp after updated: 1700390670

thread 'main' panicked at '2. Decode: token has expired', src/main.rs:145:48
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

The number should make sense based on the values of <code>SECONDS_VALID_FOR</code> and <code>SECONDS_TO_SLEEP</code>.

We can try the following:

⓵ Setting <code>SECONDS_TO_SLEEP</code> to <code>6</code>, recompile and run, the first <code>panic!("1. Decode: token has expired")</code> should come into effect. 

⓶ Setting <code>SECONDS_TO_SLEEP</code> to <code>2</code>, recompile and run, both tokens stay valid during the entire execution of the program.

That's how expiration validation works. The next step is to use JWT in a web application to authenticate requests. I aim to do a full stack post in the near future.

I hope you find the information in this post helpful. Thank you for reading and stay safe as always.

✿✿✿

Feature image source:

<ul>
<li>
<a href="https://www.omgubuntu.co.uk/2022/09/ubuntu-2210-kinetic-kudu-default-wallpaper" target="_blank">https://www.omgubuntu.co.uk/2022/09/ubuntu-2210-kinetic-kudu-default-wallpaper</a>
</li>
<li>
<a href="https://in.pinterest.com/pin/337277459600111737/" target="_blank">https://in.pinterest.com/pin/337277459600111737/</a>
</li>
<li>
<a href="https://www.rust-lang.org/" target="_blank">https://www.rust-lang.org/</a>
</li>
<li>
<a href="https://www.pngitem.com/download/ibmJoR_rust-language-hd-png-download/" target="_blank">https://www.pngitem.com/download/ibmJoR_rust-language-hd-png-download/</a>
</li>
</ul>