» Rust: Build a REST API with Rocket » 2. Development » 2.12 Authentication

Authentication

Authentication in an API server is typically needed when you want to control access to certain resources or functionalities.

In other words, if you want to restrict access to certain API endpoints or data to users with special roles (e.g., administrator, moderator, or premium user), authentication is required.

Traditional User Flow

Update code

Add User domain entity in domain/model/user.rs:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
    pub id: u32,
    pub email: String,
    pub password: String,
    pub salt: String,
    pub is_admin: bool,
    pub created_at: String,
    pub updated_at: String,
}

Declare its business capabilities in domain/gateway/user_manager.rs:

use std::error::Error;

use crate::domain::model;

pub trait UserManager: Send + Sync {
    fn create_user(&self, u: &model::User) -> Result<u32, Box<dyn Error>>;
    fn get_user_by_email(&self, email: &str) -> Result<Option<model::User>, Box<dyn Error>>;
}

Add implementations in infrastructure/database/mysql.rs:

@@ -4,7 +4,7 @@ use chrono::Utc;
 use mysql::prelude::Queryable;
 use mysql::{Error as MySQLError, Pool};
 
-use crate::domain::gateway::BookManager;
+use crate::domain::gateway::{BookManager, UserManager};
 use crate::domain::model;
 
 pub struct MySQLPersistence {
@@ -145,3 +145,52 @@ impl BookManager for MySQLPersistence {
         Ok(books)
     }
 }
+
+impl UserManager for MySQLPersistence {
+    fn create_user(&self, u: &model::User) -> Result<u32, Box<dyn Error>> {
+        let mut conn = self.pool.get_conn()?;
+        conn.exec::<usize, &str, (String, String, String, bool, String, String)>(
+            "INSERT INTO users (email, password, salt, is_admin, created_at, updated_at)
+             VALUES (?, ?, ?, ?, ?, ?)",
+            (
+                u.email.clone(),
+                u.password.clone(),
+                u.salt.clone(),
+                u.is_admin,
+                u.created_at.clone(),
+                u.updated_at.clone(),
+            ),
+        )?;
+        Ok(conn.last_insert_id() as u32)
+    }
+
+    fn get_user_by_email(&self, email: &str) -> Result<Option<model::User>, Box<dyn Error>> {
+        let mut conn = self.pool.get_conn()?;
+        let users = conn.query_map(
+            format!(
+                "SELECT * FROM users WHERE email = '{}'",
+                email.replace("'", "")
+            ),
+            |(id, email, password, salt, is_admin, created_at, updated_at): (
+                u64,
+                String,
+                String,
+                String,
+                bool,
+                String,
+                String,
+            )| {
+                model::User {
+                    id: id as u32,
+                    email,
+                    password,
+                    salt,
+                    is_admin,
+                    created_at,
+                    updated_at,
+                }
+            },
+        )?;
+        Ok(users.first().cloned())
+    }
+}

Add dependencies rand, sha1 and hex:

cargo add rand sha1 hex

Diff in Cargo.toml:

@@ -7,12 +7,15 @@ edition = "2021"
 
 [dependencies]
 chrono = { version = "0.4.35", features = ["serde"] }
+hex = "0.4.3"
 lazy_static = "1.4.0"
 mongodb = { version = "2.8.2", default-features = false, features = ["sync"] }
 mysql = "24.0.0"
+rand = "0.8.5"
 redis = "0.25.2"
 rocket = { version = "0.5.0", features = ["json"] }
 rusqlite = "0.31.0"
 serde = { version = "1.0.197", features = ["derive"] }
 serde_json = "1.0.114"
+sha1 = "0.10.6"
 toml = "0.8.11"

Do the sign-up and sign-in preparation work in application/executor/user_operator.rs:

use std::error::Error;
use std::sync::Arc;

use rand::{thread_rng, Rng};
use sha1::{Digest, Sha1};

use crate::application::dto;
use crate::domain::{gateway, model};

const SALT_LEN: usize = 4;
const ERR_EMPTY_EMAIL: &str = "empty email";
const ERR_EMPTY_PASSWORD: &str = "empty password";

pub struct UserOperator {
    user_manager: Arc<dyn gateway::UserManager>,
}

impl UserOperator {
    pub fn new(u: Arc<dyn gateway::UserManager>) -> Self {
        UserOperator { user_manager: u }
    }

    pub fn create_user(&self, uc: &dto::UserCredential) -> Result<dto::User, Box<dyn Error>> {
        if uc.email.is_empty() {
            return Err(ERR_EMPTY_EMAIL.into());
        }
        if uc.password.is_empty() {
            return Err(ERR_EMPTY_PASSWORD.into());
        }
        let salt = random_string(SALT_LEN);
        let user = model::User {
            id: 0,
            email: uc.email.clone(),
            password: sha1_hash(&(uc.password.clone() + &salt)),
            salt,
            is_admin: false,
            created_at: chrono::Utc::now()
                .format("%Y-%m-%d %H:%M:%S%.3f")
                .to_string(),
            updated_at: chrono::Utc::now()
                .format("%Y-%m-%d %H:%M:%S%.3f")
                .to_string(),
        };
        let uid = self.user_manager.create_user(&user)?;
        Ok(dto::User {
            id: uid,
            email: uc.email.clone(),
        })
    }

    pub fn sign_in(&self, email: &str, password: &str) -> Result<dto::User, Box<dyn Error>> {
        if email.is_empty() {
            return Err(ERR_EMPTY_EMAIL.into());
        }
        if password.is_empty() {
            return Err(ERR_EMPTY_PASSWORD.into());
        }
        let user = self.user_manager.get_user_by_email(email)?;
        if let Some(u) = user {
            let password_hash = sha1_hash(&(password.to_string() + &u.salt));
            if u.password != password_hash {
                return Err("wrong password".into());
            }
            Ok(dto::User {
                id: u.id,
                email: u.email,
            })
        } else {
            Err("user does not exist".into())
        }
    }
}

fn random_string(length: usize) -> String {
    let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let mut rng = thread_rng();
    (0..length)
        .map(|_| rng.gen::<usize>() % charset.len())
        .map(|idx| charset.chars().nth(idx).unwrap())
        .collect()
}

fn sha1_hash(input: &str) -> String {
    let mut h = Sha1::new();
    h.update(input);
    let hash_bytes = h.finalize();
    hex::encode(hash_bytes)
}

func random_string and sha1_hash are helper functions for salt generation and password hashing.

Why do you need a salt?

Without a salt, an attacker could precompute hashes for common passwords and store them in a table (known as a rainbow table). If the database is compromised, the attacker can then compare the hashed passwords against the precomputed hashes to quickly identify passwords. Adding a salt to each password ensures that even if two users have the same password, their hashed passwords will be different due to the unique salt.

Even if a user chooses a weak password, such as a common dictionary word, the salt ensures that the resulting hash is unique. Without a salt, all instances of the same password would hash to the same value, making them vulnerable to attacks.

Add DTOs in application/dto/user.rs:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UserCredential {
    pub email: String,
    pub password: String,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
    pub id: u32,
    pub email: String,
}

DTO stands for Data Transfer Object.

Often, the internal representation of data within your application might differ from what you want to expose through your API. DTOs allow you to transform your internal data structures into a format that's more suitable for the API consumers. This helps in decoupling the internal representation from the external one, allowing for better flexibility and abstraction.

Wire in new dependencies in application/wire_helper.rs:

@@ -31,6 +31,10 @@ impl WireHelper {
         Arc::clone(&self.sql_persistence) as Arc<dyn gateway::BookManager>
     }
 
+    pub fn user_manager(&self) -> Arc<dyn gateway::UserManager> {
+        Arc::clone(&self.sql_persistence) as Arc<dyn gateway::UserManager>
+    }
+
     pub fn review_manager(&self) -> Arc<dyn gateway::ReviewManager> {
         Arc::clone(&self.no_sql_persistence) as Arc<dyn gateway::ReviewManager>
     }

Finnally, add routes in adapter/router.rs:

@@ -10,6 +10,7 @@ use crate::domain::model;
 pub struct RestHandler {
     book_operator: executor::BookOperator,
     review_operator: executor::ReviewOperator,
+    user_operator: executor::UserOperator,
 }
 
 #[derive(serde::Serialize)]
@@ -219,6 +220,38 @@ pub fn delete_review(
     }
 }
 
+#[post("/users", format = "json", data = "<uc>")]
+pub fn user_sign_up(
+    rest_handler: &rocket::State<RestHandler>,
+    uc: Json<dto::UserCredential>,
+) -> Result<Json<dto::User>, status::Custom<Json<ErrorResponse>>> {
+    match rest_handler.user_operator.create_user(&uc.into_inner()) {
+        Ok(u) => Ok(Json(u)),
+        Err(err) => Err(status::Custom(
+            Status::InternalServerError,
+            Json(ErrorResponse {
+                error: err.to_string(),
+            }),
+        )),
+    }
+}
+
+#[post("/users/sign-in", format = "json", data = "<uc>")]
+pub fn user_sign_in(
+    rest_handler: &rocket::State<RestHandler>,
+    uc: Json<dto::UserCredential>,
+) -> Result<Json<dto::User>, status::Custom<Json<ErrorResponse>>> {
+    match rest_handler.user_operator.sign_in(&uc.email, &uc.password) {
+        Ok(u) => Ok(Json(u)),
+        Err(err) => Err(status::Custom(
+            Status::InternalServerError,
+            Json(ErrorResponse {
+                error: err.to_string(),
+            }),
+        )),
+    }
+}
+
 pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
     RestHandler {
         book_operator: executor::BookOperator::new(
@@ -226,5 +259,6 @@ pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
             wire_helper.cache_helper(),
         ),
         review_operator: executor::ReviewOperator::new(wire_helper.review_manager()),
+        user_operator: executor::UserOperator::new(wire_helper.user_manager()),
     }
 }

Try with curl

User sign up

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-rust@example.com", "password": "test-pass"}' http://localhost:8000/users

Result:

{"id":11,"email":"test-rust@example.com"}

If you check out the records in the database, you will find something like this:

+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+
| id | email                 | password                                 | salt | is_admin | created_at              | updated_at              |
+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+
| 11 | test-rust@example.com | bb426d1c455aa993de2f2b47b815bbc299d768bc | 8aUa |        0 | 2024-03-20 04:40:17.060 | 2024-03-20 04:40:17.060 |
+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+

User sign in successfully

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "test-pass"}' http://localhost:8000/users/sign-in

Result:

{"id":2,"email":"test-user@example.com"}

User sign in with an incorrect password

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "wrong-pass"}' http://localhost:8000/users/sign-in

Result:

{"error":"wrong password"}

User sign in with a non-existent email

curl -X POST -H "Content-Type: application/json" -d '{"email": "non-existent@example.com", "password": "test-pass"}' http://localhost:8000/users/sign-in 

Result:

{"error":"user does not exist"}

Note:
It's good practice to use a uniform JSON response format for both successful and failed responses.
e.g.

{"code":0, "message":"ok", "data": {"id":2,"email":"test-user@example.com"}}
{"code":1001, "message":"wrong password", "data": null}

Use JWT (JSON Web Token)

JWT, or JSON Web Token, is a compact, URL-safe means of representing claims to be transferred between two parties. It's often used for authentication and authorization in web applications and APIs.

Install JWT dependency

cargo add jsonwebtoken

Diff in Cargo.toml:

@@ -8,6 +8,7 @@ edition = "2021"
 [dependencies]
 chrono = { version = "0.4.35", features = ["serde"] }
 hex = "0.4.3"
+jsonwebtoken = "9.2.0"
 lazy_static = "1.4.0"
 mongodb = { version = "2.8.2", default-features = false, features = ["sync"] }
 mysql = "24.0.0"

Update code

Add UserPermission enums in domain/model/user.go:

@@ -1,3 +1,12 @@
+// UserPermission represents different levels of user permissions.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
+pub enum UserPermission {
+    PermNone,
+    PermUser,
+    PermAuthor,
+    PermAdmin,
+}
+
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct User {
     pub id: u32,

Declare its business capabilities in domain/gateway/user_manager.rs:

@@ -6,3 +6,18 @@ pub trait UserManager: Send + Sync {
     fn create_user(&self, u: &model::User) -> Result<u32, Box<dyn Error>>;
     fn get_user_by_email(&self, email: &str) -> Result<Option<model::User>, Box<dyn Error>>;
 }
+
+pub trait PermissionManager: Send + Sync {
+    fn generate_token(
+        &self,
+        user_id: u32,
+        email: &str,
+        perm: model::UserPermission,
+    ) -> Result<String, Box<dyn Error>>;
+
+    fn has_permission(
+        &self,
+        token: &str,
+        perm: model::UserPermission,
+    ) -> Result<bool, Box<dyn Error>>;
+}

Implement these 2 methods in infrastructure/token/jwt.rs:

use std::error::Error;
use std::time::{Duration, SystemTime};

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

use crate::domain::gateway::PermissionManager;
use crate::domain::model;

// Keeper manages user tokens.
pub struct Keeper {
    secret_key: String,
    expire_hours: u64,
}

// UserClaims includes user info.
#[derive(Debug, Serialize, Deserialize)]
pub struct UserClaims {
    user_id: u32,
    user_name: String,
    permission: model::UserPermission,
    exp: usize, // Expiry time in seconds since epoch
}

impl Keeper {
    // NewTokenKeeper constructs a new JWT token keeper
    pub fn new(secret_key: String, expire_in_hours: u32) -> Self {
        Keeper {
            secret_key: secret_key,
            expire_hours: expire_in_hours as u64,
        }
    }

    // extract_token extracts the token from the signed string.
    fn extract_token(&self, token_result: &str) -> Result<UserClaims, Box<dyn Error>> {
        let token_data = decode::<UserClaims>(
            token_result,
            &DecodingKey::from_secret(self.secret_key.as_ref()),
            &Validation::default(),
        )?;
        Ok(token_data.claims)
    }
}

impl PermissionManager for Keeper {
    // generate_token generates a new JWT token.
    fn generate_token(
        &self,
        user_id: u32,
        email: &str,
        perm: model::UserPermission,
    ) -> Result<String, Box<dyn Error>> {
        let exp = SystemTime::now()
            .checked_add(Duration::from_secs(self.expire_hours * 3600))
            .ok_or("Overflow when adding expire time")?;
        let exp = exp.duration_since(SystemTime::UNIX_EPOCH)?.as_secs() as usize;

        let claims = UserClaims {
            user_id,
            user_name: email.to_owned(),
            permission: perm,
            exp,
        };

        let header = Header::default();
        let token = encode(
            &header,
            &claims,
            &EncodingKey::from_secret(self.secret_key.as_ref()),
        )?;
        Ok(token)
    }

    // has_permission checks if user has the given permission.
    fn has_permission(
        &self,
        token_result: &str,
        perm: model::UserPermission,
    ) -> Result<bool, Box<dyn Error>> {
        let claims = self.extract_token(token_result)?;
        Ok(claims.permission >= perm)
    }
}

Add config items for Tokens in infrastructure/config/mod.rs:

@@ -26,6 +26,8 @@ pub struct CacheConfig {
 pub struct ApplicationConfig {
     pub port: i32,
     pub page_size: u32,
+    pub token_secret: String,
+    pub token_hours: u32,
 }
 
 pub fn parse_config(file_name: &str) -> Config {

Put in setting values in config.toml:

@@ -1,6 +1,8 @@
 [app]
 port = 8080
 page_size = 5
+token_secret = "I_Love_LiteRank"
+token_hours = 48
 
 [db]
 file_name = "test.db"

Generate tokens and return them in application/executor/user_operator.rs:

@@ -13,11 +13,15 @@ const ERR_EMPTY_PASSWORD: &str = "empty password";
 
 pub struct UserOperator {
     user_manager: Arc<dyn gateway::UserManager>,
+    perm_manager: Arc<dyn gateway::PermissionManager>,
 }
 
 impl UserOperator {
-    pub fn new(u: Arc<dyn gateway::UserManager>) -> Self {
-        UserOperator { user_manager: u }
+    pub fn new(u: Arc<dyn gateway::UserManager>, p: Arc<dyn gateway::PermissionManager>) -> Self {
+        UserOperator {
+            user_manager: u,
+            perm_manager: p,
+        }
     }
 
     pub fn create_user(&self, uc: &dto::UserCredential) -> Result<dto::User, Box<dyn Error>> {
@@ -48,7 +52,7 @@ impl UserOperator {
         })
     }
 
-    pub fn sign_in(&self, email: &str, password: &str) -> Result<dto::User, Box<dyn Error>> {
+    pub fn sign_in(&self, email: &str, password: &str) -> Result<dto::UserToken, Box<dyn Error>> {
         if email.is_empty() {
             return Err(ERR_EMPTY_EMAIL.into());
         }
@@ -61,14 +65,31 @@ impl UserOperator {
             if u.password != password_hash {
                 return Err("wrong password".into());
             }
-            Ok(dto::User {
-                id: u.id,
-                email: u.email,
+            let perm = if u.is_admin {
+                model::UserPermission::PermAdmin
+            } else {
+                model::UserPermission::PermUser
+            };
+            let token = self.perm_manager.generate_token(u.id, &u.email, perm)?;
+            Ok(dto::UserToken {
+                user: dto::User {
+                    id: u.id,
+                    email: u.email,
+                },
+                token,
             })
         } else {
             Err("user does not exist".into())
         }
     }
+
+    pub fn has_permission(
+        &self,
+        token: &str,
+        perm: model::UserPermission,
+    ) -> Result<bool, Box<dyn Error>> {
+        self.perm_manager.has_permission(token, perm)
+    }
 }
 
 fn random_string(length: usize) -> String {

Add DTOs in application/dto/user.rs:

@@ -9,3 +9,9 @@ pub struct User {
     pub id: u32,
     pub email: String,
 }
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct UserToken {
+    pub user: User,
+    pub token: String,
+}

Tune application/wire_helper.rs:

@@ -3,12 +3,14 @@ use std::sync::Arc;
 use crate::domain::gateway;
 use crate::infrastructure::cache;
 use crate::infrastructure::database;
+use crate::infrastructure::token;
 use crate::infrastructure::Config;
 
 pub struct WireHelper {
     sql_persistence: Arc<database::MySQLPersistence>,
     no_sql_persistence: Arc<database::MongoPersistence>,
     kv_store: Arc<cache::RedisCache>,
+    token_keeper: Arc<token::Keeper>,
 }
 
 impl WireHelper {
@@ -20,10 +22,15 @@ impl WireHelper {
             &c.db.mongo_db_name,
         )?);
         let kv_store = Arc::new(cache::RedisCache::new(&c.cache.redis_uri)?);
+        let token_keeper = Arc::new(token::Keeper::new(
+            c.app.token_secret.clone(),
+            c.app.token_hours,
+        ));
         Ok(WireHelper {
             sql_persistence,
             no_sql_persistence,
             kv_store,
+            token_keeper,
         })
     }
 
@@ -35,6 +42,10 @@ impl WireHelper {
         Arc::clone(&self.sql_persistence) as Arc<dyn gateway::UserManager>
     }
 
+    pub fn perm_manager(&self) -> Arc<dyn gateway::PermissionManager> {
+        Arc::clone(&self.token_keeper) as Arc<dyn gateway::PermissionManager>
+    }
+
     pub fn review_manager(&self) -> Arc<dyn gateway::ReviewManager> {
         Arc::clone(&self.no_sql_persistence) as Arc<dyn gateway::ReviewManager>
     }

Add PermCheck middleware into routes for Create/Update/Delete operations in adapter/router.rs:

@@ -2,6 +2,7 @@ use rocket::http::Status;
 use rocket::response::{content, status};
 use rocket::serde::json::Json;
 
+use crate::adapter::middleware::PermCheck;
 use crate::application;
 use crate::application::dto;
 use crate::application::executor;
@@ -10,7 +11,7 @@ use crate::domain::model;
 pub struct RestHandler {
     book_operator: executor::BookOperator,
     review_operator: executor::ReviewOperator,
-    user_operator: executor::UserOperator,
+    pub user_operator: executor::UserOperator,
 }
 
 #[derive(serde::Serialize)]
@@ -73,6 +74,7 @@ pub fn get_book(
 pub fn create_book(
     rest_handler: &rocket::State<RestHandler>,
     book: Json<model::Book>,
+    _perm_check: PermCheck,
 ) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
     match rest_handler.book_operator.create_book(book.into_inner()) {
         Ok(b) => Ok(Json(b)),
@@ -90,6 +92,7 @@ pub fn update_book(
     rest_handler: &rocket::State<RestHandler>,
     id: u32,
     book: Json<model::Book>,
+    _perm_check: PermCheck,
 ) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
     match rest_handler
         .book_operator
@@ -109,6 +112,7 @@ pub fn update_book(
 pub fn delete_book(
     rest_handler: &rocket::State<RestHandler>,
     id: u32,
+    _perm_check: PermCheck,
 ) -> Result<status::NoContent, status::Custom<Json<ErrorResponse>>> {
     match rest_handler.book_operator.delete_book(id) {
         Ok(_) => Ok(status::NoContent),
@@ -240,7 +244,7 @@ pub fn user_sign_up(
 pub fn user_sign_in(
     rest_handler: &rocket::State<RestHandler>,
     uc: Json<dto::UserCredential>,
-) -> Result<Json<dto::User>, status::Custom<Json<ErrorResponse>>> {
+) -> Result<Json<dto::UserToken>, status::Custom<Json<ErrorResponse>>> {
     match rest_handler.user_operator.sign_in(&uc.email, &uc.password) {
         Ok(u) => Ok(Json(u)),
         Err(err) => Err(status::Custom(
@@ -259,6 +263,9 @@ pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
             wire_helper.cache_helper(),
         ),
         review_operator: executor::ReviewOperator::new(wire_helper.review_manager()),
-        user_operator: executor::UserOperator::new(wire_helper.user_manager()),
+        user_operator: executor::UserOperator::new(
+            wire_helper.user_manager(),
+            wire_helper.perm_manager(),
+        ),
     }
 }

Request guards are one of Rocket's most powerful instruments. As the name might imply, a request guard protects a handler from being called erroneously based on information contained in an incoming request. More specifically, a request guard is a type that represents an arbitrary validation policy. The validation policy is implemented through the FromRequest trait. Every type that implements FromRequest is a request guard.

PermCheck is implemented in adapter/middleware.rs:

use rocket::http::Status;
use rocket::request::{self, FromRequest, Request};

use crate::domain::model::UserPermission;
use crate::RestHandler;

// Define a struct to hold the permission level required for the route
pub struct PermCheck;

// Implement FromRequest trait to perform permission check
#[rocket::async_trait]
impl<'r> FromRequest<'r> for PermCheck {
    type Error = &'static str;

    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
        let auth_header = match request.headers().get_one("Authorization") {
            Some(header) => header,
            None => return request::Outcome::Error((Status::Unauthorized, "Token is required")),
        };
        let token = auth_header.trim_start_matches("Bearer ");
        let rest_handler = request.rocket().state::<RestHandler>().unwrap();
        // Check user permission against required permission
        match rest_handler
            .user_operator
            .has_permission(token, UserPermission::PermAuthor)
        {
            Ok(b) => {
                if b {
                    request::Outcome::Success(PermCheck {})
                } else {
                    request::Outcome::Error((Status::Unauthorized, "Unauthorized"))
                }
            }
            Err(_) => request::Outcome::Error((Status::BadRequest, "Invalid token")),
        }
    }
}

Those are all the changes you need to utilize JWT in your api server. Let's try it out again!

Try with curl

User sign in and get a Token

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "test-pass"}' http://localhost:8000/users/sign-in

Result:

{
  "user": { "id": 2, "email": "test-user@example.com" },
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoiUGVybUFkbWluIiwiZXhwIjoxNzExMDk4NjU5fQ.cHaxxc44Sd7sFWe6vq1NTgFad3HUC7ghX8TJK96_EiU"
}

Put the token into Debugger on https://jwt.io/, then you can check the payload and verify the signature of your token.

JWT Debugger

Create a new book with a valid and proper Token

curl -X POST \
  http://localhost:8000/books \
  -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoiUGVybUFkbWluIiwiZXhwIjoxNzExMDk4NjU5fQ.cHaxxc44Sd7sFWe6vq1NTgFad3HUC7ghX8TJK96_EiU' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": 0,
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100,
    "created_at": "",
    "updated_at": ""
}'

Tip: You may add DTOs to make this json body cleaner.

Successful result:

{
  "id": 21,
  "title": "Test Book",
  "author": "John Doe",
  "published_at": "2003-01-01",
  "description": "A sample book description",
  "isbn": "1234567890",
  "total_pages": 100,
  "created_at": "",
  "updated_at": ""
}

Create a new book with a Token, but it doesn't have proper permissions

curl -X POST \
  http://localhost:8000/books \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoxLCJleHAiOjE3MDkyNzA1MzV9.eJp5Yr9YZMxHFs5eId78bEJ6draO178jysquZ2VV9v8' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Rocket server logs:

POST /books application/json:
   >> Matched: (create_book) POST /books application/json
   >> Request guard `PermCheck` failed: "Invalid token".
   >> Outcome: Error(400 Bad Request)
   >> No 400 catcher registered. Using Rocket default.
   >> Response succeeded.

Create a new book without a Token

curl -X POST \
  http://localhost:8000/books \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Rocket server logs:

POST /books application/json:
   >> Matched: (create_book) POST /books application/json
   >> Request guard `PermCheck` failed: "Token is required".
   >> Outcome: Error(401 Unauthorized)
   >> No 401 catcher registered. Using Rocket default.
   >> Response succeeded.

Create a new book with a fake Token

curl -X POST \
  http://localhost:8000/books \
  -H 'Authorization: Bearer FAKE_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Rocket server logs:

POST /books application/json:
   >> Matched: (create_book) POST /books application/json
   >> Request guard `PermCheck` failed: "Invalid token".
   >> Outcome: Error(400 Bad Request)
   >> No 400 catcher registered. Using Rocket default.
   >> Response succeeded.

Perfect! 🎉

Now, some of your api endpoints are protected by the authentication mechanism.