Build an API in Rust (Part 3)

Welcome to the third and last part of the guide on how to build an API in Rust!

To follow this guide you will need to have the code from the second part. If you haven't checked it out yet, please do!

This time I will explain how to protect the POST /items endpoint with a JWT (JSON Web Token).

Again, I will put all the code in the same file (src/main.rs).

Prerequisites

To be able to follow the guide, you will need to have Rust and Cargo installed. You will also need to have a MongoDB running. If you need help installing MongoDB, I've covered this in the second part of the guide

Set up the environment

We need to add a new environment variable in the .env file. Open the .env file, then under the MONGODB_URI add a new variable named JWT_SECRET and set the value to anything you like.

Add the new dependency

We need to add one new dependency. jsonwebtoken.

jsonwebtoken is a crate that allows us to encode, decode and validate JWTs.

Update the Cargo.toml file with the new dependency. The updated dependencies should be like this

[dependencies]
tide = "0.16"
async-std = { version = "1", features = ["attributes"] }
serde = { version = "1", features = ["derive"] }
dotenv = "0.15"
mongodb = { version = "1", features = ["async-std-runtime"], default-features = false }
jsonwebtoken = "7"

Let's code

We need to make some modifications to the code. First update the imports with the following

use async_std::stream::StreamExt;
use dotenv::dotenv;
use jsonwebtoken;
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use std::{env, future::Future, pin::Pin};
use tide::{Body, Next, Request, Response, StatusCode};

These are all the module items we will use in this walk-through.

We need to create a new struct that defines the payload of the JWT. I will not be adding anything in the payload so it will just be an empty struct.

Add the following somewhere in the src/main.rs file

#[derive(Serialize, Deserialize)]
struct TokenClaims {}

Now that we have the TokenClaims we need to write the middleware function. Add the following in the src/main.rs

fn auth_middleware<'a>(
req: Request<State>,
next: Next<'a, State>,
) -> Pin<Box<dyn Future<Output = tide::Result> + Send + 'a>> {
return Box::pin(async {
// Retrieve the "Authorization" header from the request
let authorization_header = req.header("Authorization");

// Check that the "Authorization" header is not missing
// if it is missing, respond with 401
let authorization_header = match authorization_header {
Some(h) => h.as_str(),
None => {
return Ok(Response::new(StatusCode::Unauthorized));
}
};

// Attempt to remove the "Bearer " prefix
// if it does not start with "Bearer " respond with 401
let token = match authorization_header.strip_prefix("Bearer ") {
Some(t) => t,
None => {
return Ok(Response::new(StatusCode::Unauthorized));
}
};

// Retrieve the "JWT_SECRET" environment variable
let secret = env::var("JWT_SECRET").unwrap();

// Decode the JWT
let token = jsonwebtoken::decode::<TokenClaims>(
token,
&jsonwebtoken::DecodingKey::from_secret(secret.as_ref()),
// Do not require the "exp" claim in the token payload
&jsonwebtoken::Validation {
validate_exp: false,
..Default::default()
},
);

// If there was an error when decoding the token respond with 401
if token.is_err() {
return Ok(Response::new(StatusCode::Unauthorized));
}

// The request is authorized, proceed with the next controller
return Ok(next.run(req).await);
});
}

I've added comments to explain what's going on.

To summarize, the middleware checks that there is an Authorization header in the request. It then tries to get the JWT from the header. Finally using the jsonwebtoken crate, it attempts to decode the JWT. If there is any error during the process, the function responds with a 401 HTTP status code for Unauthorized. Otherwise the request is authorized and the middleware runs the next controller in the chain.

We have to change the main function to use this new middleware, update the POST /items route like this

app.at("/items").with(auth_middleware).post(post_item);

Let's test it out! Start the server with the following command

cargo run

If you try to send a POST request on /items (http://localhost:8080/items) you should get a 401 status code.

You need to generate a JWT with the secret you've set up in the .env file. An easy way to do this is to head over to jwt.io, change the payload to an empty JSON object {} and replace the default secret with yours. You can then copy the token.

Before sending the request, make sure to add an Authorization header with the following value Bearer YOUR_JWT_TOKEN. Of course replace YOUR_JWT_TOKEN with the actual token.

Now if you try to send the POST request again you should get a 200 status code!

In conclusion

That's it! This is the end of the guide, I hope it was helpful. If you noticed any error or something that could be improved, please let me know!

You can find the the full example of this walk-through on GitHub: https://github.com/ncribt/rust-api-example-part-3