Using Rust and Axum to build a JWT authentication API
Tags: rust, jwt, webdev
Published: 2024-06-05 14:16:51

**Written by [Eze Sunday](https://blog.logrocket.com/author/ezesunday/)✏️**

Building a non-trivial web application with Rust can be fairly straightforward. However, when things become complex and require features like authentication, middleware, and more, that’s where Axum shines. Axum makes it a lot easier to build complex web API authentication systems. In this step-by-step guide, we'll build a JWT authentication API using [Rust and the Axum framework](https://blog.logrocket.com/rust-axum-error-handling/). We'll cover everything from building the authentication endpoints to JWT middleware and protected routes. 

Let’s jump right in.

## Setting up our Rust and Axum project

Let’s start by installing Rust, Axum, and all the necessary dependencies. Run the following commands to install Rust if you don’t already have Rust installed:

```shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

The above command requires internet to do. It’ll download and set up Rust and all the tools needed for Rust development, except for your [code editor](https://blog.logrocket.com/how-to-debug-rust-vs-code/) 🙂 

Next, run the command below to create a new Rust project and install all the dependencies necessary for this project:

```rust
cargo new rust-auth && cd rust-auth && cargo add tokio --features full && cargo add serde@1.0.195 --features derive && cargo add chrono@0.4.34 --features serde && cargo add axum@0.7.5  jsonwebtoken@9.3.0 bcrypt@0.15.1 serde_json@1.0.95
```

The result should generate a directory like this:

```shell
.
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs
```

And the `Cargo.toml` file should look like this:

```toml
[package]
name = "rust-auth"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7.5"
bcrypt = "0.15.1"
chrono = { version = "0.4.34", features = ["serde"] }
jsonwebtoken = "9.3.0"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.95"
tokio = { version = "1.37.0", features = ["full"] }
```

Here is a quick rundown of the dependencies we added and why we added each one:

*   **Axum**: This is the core Axum Web framework we’ll use for this project
*   **Tokio**: We’ll use the Rust Tokio runtime to write asynchronous functions
*   **Serde**: We’ll use Serde for our serialization and deserialization needs
*   **Chrono**: We’ll also need the date and time library for different things in our application. Specifically, we’ll use it to generate our API token expiry time
*   **JSON Web Token (jsonwebtoken)**: The jsonwebtoken library will help us generate and verify JSON Web Tokens
*   **BCrypt**: Since we’ll be integrating password hashing, we’ll use BCrypt password hashing function for that
*   **Serde JSON**: We'll eventually need to return the API response to the client via a REST API. So, we'll use the Serde JSON library to convert from other data structures to JSON and return the response

Now that we have our setup completed, let’s create the relevant endpoints for our project.

## Authentication endpoints using Axum middleware

We’ll have a route for the user to login, as well as a protected route to demonstrate how to protect our endpoints using the Axum middleware system.

### Tokio and Axum server setup

Before we proceed with that, let’s create the web server with Tokio and Axum in the `main.rs` file. First off, here’s the basic server anatomy: ![Example Of A Simple Server Set Up Using Tokio And Axum In A Rust Project](https://blog.logrocket.com/wp-content/uploads/2024/05/img1-Simple-server-setup-Tokio-Axum-Rust.jpeg) For our specific project, copy the code below and replace your existing code in the `main.rs` file with it:

```rust
use axum;
use tokio::net::TcpListener;
mod routes;

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080")
        .await
        .expect("Unable to connect to the server");
    let app = routes::app().await;

    axum::serve(listener, app)
        .await
        .expect("Error serving application");

    println!("Listening on {}", listener.local_addr().unwrap() );
}
```

The above code uses Tokio’s TCP listener bound to the address `127.0.0.1:8080` and then uses Axum to serve the web app. It also imports the routes definition which is where will set our focus now.

### Authentication routes

Let’s define the different routes we’ll use for our authentication. Basically, the flow will enable the user to:

*   Sign in and receive a token (the `/signin` route)
*   Use the token to access protected endpoints (the `/protected/` route)

In that case, we’ll have two endpoints — let’s create them! Create a `routes.rs` file in the `src/` directory and add the following code in it:

```rust
use axum::{
    middleware,
    routing::{get, post},
    Router,
};
use crate::{auth, services};

pub async fn app() -> Router {
    Router::new()
        .route("/signin", post(auth::sign_in))
        .route(
            "/protected/",
            get(services::hello).layer(middleware::from_fn(auth::authorize)),
        )
}
```

The code above contains the two routes definition with their handlers. Notice that there is a middleware in the `/protected` endpoint (auth::authorize) — we’ll take a look at that in a minute. 

We’ve imported the `auth` and `hello` services — that’s were we’ll implement the handlers. Let’s create them:

## Authentication handlers

Create a `services.rs` file and add the code below to create the `hello` handler:

```rust
use crate::auth::CurrentUser;
#[derive(Serialize, Deserialize)]
struct UserResponse {
    email: String, 
    first_name: String,
    last_name: String
}

pub async fn hello(Extension(currentUser): Extension<CurrentUser>) -> impl IntoResponse {
    Json(UserResponse {
        email: currentUser.email,
        first_name: currentUser.first_name,
        last_name: currentUser.last_name
    })
}
```

The `hello` handler returns the logged in user’s profile information. When we call the protected route with the user JWT token, the server will return the user information like so: ![User Information Returned By The Server](https://blog.logrocket.com/wp-content/uploads/2024/05/img2-User-information-returned-server.png) The next service is `auth`. This service contains all the implementations for our JWT authentication. This is where you’ll need to pay closer attention 😁 

Create the `auth.rs` file in the `src/` directory. Then, add the code to sign a user in with their username and password as shown below:

```rust
use axum::{
    body::Body,
    response::IntoResponse,
    extract::{Request, Json},
    http,
    http::{Response, StatusCode},
    middleware::Next,
};
use bcrypt::{hash, verify, DEFAULT_COST};
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize};
use serde_json::json;

#[derive(Serialize, Deserialize)]
// Define a structure for holding claims data used in JWT tokens
pub struct Claims {
    pub exp: usize,  // Expiry time of the token
    pub iat: usize,  // Issued at time of the token
    pub email: String,  // Email associated with the token
}

// Define a structure for holding sign-in data
#[derive(Deserialize)]
pub struct SignInData {
    pub email: String,  // Email entered during sign-in
    pub password: String,  // Password entered during sign-in
}

// Function to handle sign-in requests
pub async fn sign_in(
    Json(user_data): Json<SignInData>,  // JSON payload containing sign-in data
) -> Result<Json<String>, StatusCode> {  // Return type is a JSON-wrapped string or an HTTP status code

    // Attempt to retrieve user information based on the provided email
    let user = match retrieve_user_by_email(&user_data.email) {
        Some(user) => user,  // User found, proceed with authentication
        None => return Err(StatusCode::UNAUTHORIZED), // User not found, return unauthorized status
    };

    // Verify the password provided against the stored hash
    if !verify_password(&user_data.password, &user.password_hash)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? // Handle bcrypt errors
    {
        return Err(StatusCode::UNAUTHORIZED); // Password verification failed, return unauthorized status
    }

    // Generate a JWT token for the authenticated user
    let token = encode_jwt(user.email)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Handle JWT encoding errors

    // Return the token as a JSON-wrapped string
    Ok(Json(token))
}

#[derive(Clone)]
pub struct CurrentUser {
    pub email: String,
    pub first_name: String,
    pub last_name: String,
    pub password_hash: String
}

// Function to simulate retrieving user data from a database based on email
fn retrieve_user_by_email(email: &str) -> Option<CurrentUser> {
    // For demonstration purposes, a hardcoded user is returned based on the provided email
    let current_user: CurrentUser = CurrentUser {
        email: "myemail@gmail.com".to_string(),
        first_name: "Eze".to_string(),
        last_name: "Sunday".to_string(),
        password_hash: "$2b$12$Gwf0uvxH3L7JLfo0CC/NCOoijK2vQ/wbgP.LeNup8vj6gg31IiFkm".to_string()
    };
    Some(current_user) // Return the hardcoded user
}

```

Although the code is a bit long, it is heavily commented to make it easy to understand and follow along. Now, we are going to explain every part of it. 

First, we created the `Claims` and `SignInData` data struct. 

The `Claims` is what we expect to be encoded in the JWT token. We want the expiry, issue date/time, and email to be in `Claims`. We expect the user to send their email address and password in exchange for the JWT token:

```rust
// Define a structure for holding claims data used in JWT tokens
pub struct Claims {
    pub exp: usize,  // Expiry time of the token
    pub iat: usize,  // Issued at time of the token
    pub email: String,  // Email associated with the token
}
```

The `SignInData` struct represents that data, as shown below in this extracted code from the previous code:

```rust
// Define a structure for holding sign-in data
#[derive(Deserialize)]
pub struct SignInData {
    pub email: String,  // Email entered during sign-in
    pub password: String,  // Password entered during sign-in
}
```

Next, we get into the sign-in function. The sign-in function accepts a JSON object as the request body and returns a JSON or StatusCode as shown below:

```rust
// Function to handle sign-in requests
pub async fn sign_in(
    Json(user_data): Json<SignInData>,  // JSON payload containing sign-in data
) -> Result<Json<String>, StatusCode> {
```

In the sign-in function, we attempt to get the users information from the database based on their email address they provided. If it does not exist, we don’t proceed with the login. 

If it does exist, we want to verify the password the user sent with the hashed password in the database. For simplicity, we simulated the database user retrieval with the `retrieve_user_by_email` function below:

```rust
// Function to simulate retrieving user data from a database based on email
fn retrieve_user_by_email(email: &str) -> Option<CurrentUser> {
    // For demonstration purposes, a hardcoded user is returned based on the provided email
    let current_user: CurrentUser = CurrentUser {
        email: "myemail@gmail.com".to_string(),
        first_name: "Eze".to_string(),
        last_name: "Sunday".to_string(),
        password_hash: "$2b$12$Gwf0uvxH3L7JLfo0CC/NCOoijK2vQ/wbgP.LeNup8vj6gg31IiFkm".to_string() // the plain password hashed to this is "okon" without the quotes.
    };
    Some(current_user) // Return the hardcoded user
}
```

We used bcrypt password hashing to generate the password for this example. The password in the above example is `okon`. Also, below are the functions for hashing and verifying a password. Add them to the `auth.rs` file:

```rust
pub fn verify_password(password: &str, hash: &str) -> Result<bool, bcrypt::BcryptError> {
    verify(password, hash)
}
pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
    let hash = hash(password, DEFAULT_COST)?;
    Ok(hash)
}
```

Finally, we generate the JWT token and encode the user’s email into it:

```rust
let token = encode_jwt(user.email)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Handle JWT encoding errors
```

But the `encode_jwt` function isn’t created yet, so we need to create it. Add the `encode_jwt` function to the `auth.rs` file:

```rust
pub fn encode_jwt(email: String) -> Result<String, StatusCode> {
    let secret: String = "randomStringTypicallyFromEnv".to_string();
    let now = Utc::now();
    let expire: chrono::TimeDelta = Duration::hours(24);
    let exp: usize = (now + expire).timestamp() as usize;
    let iat: usize = now.timestamp() as usize;
    let claim = Cliams { iat, exp, email };

    encode(
        &Header::default(),
        &claim,
        &EncodingKey::from_secret(secret.as_ref()),
    )
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
```

This function above uses the `jwt` library to generate a valid login authentication token. The function accepts email addresses and you can add other information into the `jwt` encoding as you need. 

We’ll also need to decode the JWT when we start working on the the middleware, so, let’s create the `decode_jwt` function. Include the `decode_jwt` code below in the `auth.rs` file:

```rust
pub fn decode_jwt(jwt_token: String) -> Result<TokenData<Cliams>, StatusCode> {
    let secret = "randomStringTypicallyFromEnv".to_string();
    let result: Result<TokenData<Cliams>, StatusCode> = decode(
        &jwt_token,
        &DecodingKey::from_secret(secret.as_ref()),
        &Validation::default(),
    )
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR);
    result
}
```

Make sure the `DecodingKey` algorithm matches for both the encoding and decoding functions. The default algorithm is HS256; if you choose to use the default, you should also use the same secret for both the `encode_jwt` and `decode_jwt` functions. You can also use the RSA encryption algorithm if you need to. Here is an example of how you’d use the RSA encryption algorithm for the encoding:

```rust
let result = encode(&Header::new(Algorithm::RS256), &my_claims, &EncodingKey::from_rsa_pem(include_bytes!("privkey.pem"))?)?;
```

Here is decoding with the RSA encryption algorithm:

```rust
let result = decode::<Claims>(&jwt_token, &DecodingKey::from_rsa_components(jwk["n"], jwk["e"]), &Validation::new(Algorithm::RS256))?;
```

## Middleware for protected routes

Now that we’ve got the sign-in function in order, let’s take a closer look at the middleware function to protect our routes. Add a new function by copying and pasting the code below into the `auth.rs` file:

```rust
pub async fn authorization_middleware(mut req: Request, next: Next) -> Result<Response<Body>, AuthError> {
    let auth_header = req.headers_mut().get(http::header::AUTHORIZATION);
    let auth_header = match auth_header {
        Some(header) => header.to_str().map_err(|_| AuthError {
            message: "Empty header is not allowed".to_string(),
            status_code: StatusCode::FORBIDDEN
        })?,
        None => return Err(AuthError {
            message: "Please add the JWT token to the header".to_string(),
            status_code: StatusCode::FORBIDDEN
        }),
    };
    let mut header = auth_header.split_whitespace();
    let (bearer, token) = (header.next(), header.next());
    let token_data = match decode_jwt(token.unwrap().to_string()) {
        Ok(data) => data,
        Err(_) => return Err(AuthError {
            message: "Unable to decode token".to_string(),
            status_code: StatusCode::UNAUTHORIZED
        }),
    };
    // Fetch the user details from the database
    let current_user = match retrieve_user_by_email(&token_data.claims.email) {
        Some(user) => user,
        None => return Err(AuthError {
            message: "You are not an authorized user".to_string(),
            status_code: StatusCode::UNAUTHORIZED
        }),
    };
    req.extensions_mut().insert(current_user);
    Ok(next.run(req).await)
}
```

Let’s explain the code. The function takes a mutable `Request` and `Next` objects as arguments and returns a `Response` or `Error` result. This is a typical Axum middleware function signature. The `Next` object represents the next middleware or handler in the chain that should be called after this middleware. 

Now, we’ll grab the header content and attempt to extract the token that was passed to it by the client:

```rust
let auth_header = req.headers_mut().get(http::header::AUTHORIZATION);
```

If the token exists, we go ahead to decode the token, get the user’s email from it, and query the database to fetch the user’s profile. Then, we pass the user information to app extensions for the handler that will be using the middleware and handling the request. 

Remember the `protected` endpoint and the corresponding `hello` handler?

```rust
.route("/protected/",get(services::hello).layer(middleware::from_fn(auth::authorize_middleware)),)
```

The `hello` handler takes in an extension as an argument with the type `CurrentUser` type and returns a type of `impl IntoResponse` which is the typical return type of all Axum handlers.

```rust
pub async fn hello(Extension(currentUser): Extension<CurrentUser>) -> impl IntoResponse {
   ...
}
```

Next, let's test our implementation with Postman. If you'd love to clone the entire project and dive deep into it, or just test it out, you can clone it from GitHub by running the command below:

```shell
git clone https://github.com/ezesundayeze/axum--auth
```

## Testing with Postman

We’ve have developed two endpoints: the login endpoint and the protected endpoint. Let’s start by running the server by running the command below:

```shell
cargo run
```

And then signing in with our username and password: ![Signing Into Postman For Testing Our Rust And Axum App](https://blog.logrocket.com/wp-content/uploads/2024/05/img3-Signing-in-Postman-testing-Rust-Axum-app.png) The login returns our JWT token as expected. Next, we’ll copy the JWT token and use it to access the protected endpoint but before that, if we make the API call without the token, we’ll get an error: ![Example Error Message Shown After Making An Api Call To Access A Protected Route Without A Token](https://blog.logrocket.com/wp-content/uploads/2024/05/img4-Error-making-API-call-without-token.png) Add the token. Now we can access the protected API properly: ![Correctly Accessing The Protected Route With A Token](https://blog.logrocket.com/wp-content/uploads/2024/05/img5-Correctly-accessing-protected-route-token.png)

## Conclusion

We’ve come a long way! I hope you enjoyed reading the walkthrough and following along (if you did follow along). 

In this tutorial, we covered how to build a basic JWT authentication system from start to finish, noting all the key parts. From setting up the routes, handlers, and the middleware system, I hope this will help you bootstrap your Rust project easily. You can find the [full project on GitHub](https://github.com/ezesundayeze/axum--auth). 

Happy hacking!

---

##[LogRocket](https://lp.logrocket.com/blg/rust-signup): Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, [try LogRocket](https://lp.logrocket.com/blg/rust-signup).

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/rust-signup)

[LogRocket](https://lp.logrocket.com/blg/rust-signup) is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — [start monitoring for free](https://lp.logrocket.com/blg/rust-signup).