Hackernoon logoHow to Build a Simple API in Rust (Part 3) by@ncribt

How to Build a Simple API in Rust (Part 3)

image
NARUHODO Hacker Noon profile picture

@ncribtNARUHODO

Software engineer during the week and apprentice blogger during the weekend. I like JS and Rust. 🤓

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 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
- 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!

Conclusion

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

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

Previously published on https://naruhodo.dev/build-an-api-in-rust-part-3/.

NARUHODO Hacker Noon profile picture
by NARUHODO @ncribt. Software engineer during the week and apprentice blogger during the weekend. I like JS and Rust. 🤓Read my stories

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.