REST API Security with Rust, MongoDB, and OAuth2

by Didin J. on May 28, 2025 REST API Security with Rust, MongoDB, and OAuth2

Secure your Rust REST API with OAuth2 and MongoDB. Learn to build your own OAuth2 server using Actix-web, JWT, and hashed user credentials

In this tutorial, we’ll build a secure REST API using Rust, integrate MongoDB for persistent storage, and implement OAuth2 authentication from scratch. You’ll learn how to design and implement an OAuth2 provider that issues access tokens, protects API resources, and manages users with hashed credentials. This step-by-step guide is ideal for developers who want complete control over their authentication flow using one of the fastest and most secure languages available today.

By the end of this tutorial, you’ll have a working OAuth2-secured API with registration, login, token generation, and protected endpoints — all powered by Actix-web and MongoDB.


Project Setup Using Actix-Web

To get started, we’ll set up a new Rust project using the Actix-web framework, which is known for its speed, safety, and async-first design — perfect for building secure and performant APIs.

1. Create a New Rust Project

Open your terminal and run:

cargo new rust_oauth2_api
cd rust_oauth2_api

2. Add Dependencies

Open Cargo.toml and add the following dependencies:

[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15"
mongodb = "3.2.3"
jsonwebtoken = "9"
argon2 = "0.5"
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
futures-util = "0.3"

✅ These dependencies include everything for building routes, managing users, hashing passwords, working with MongoDB, and generating JWTs.

3. Create .env for Environment Variables

Create a .env file in your project root:

MONGO_URI=mongodb://localhost:27017
JWT_SECRET=your_very_secret_key_here

You can generate a secure JWT secret using a random string generator.

4. Set Up Basic Project Structure

Inside the src directory, create these folders:

mkdir models handlers utils middleware config
touch config/mod.rs handlers/mod.rs models/mod.rs utils/mod.rs middleware/mod.rs

Your folder structure should now look like:

src/
├── main.rs
├── config/
├── handlers/
├── middleware/
├── models/
└── utils/

5. Initialize main.rs

Here’s a minimal starter for src/main.rs:

use actix_web::{web, App, HttpServer, Responder};
use dotenv::dotenv;
use std::env;

async fn index() -> impl Responder {
    "Rust OAuth2 API is running..."
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());

    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(index))
    })
    .bind(("127.0.0.1", port.parse().unwrap()))?
    .run()
    .await
}

Run your server:

cargo run

You should see "Rust OAuth2 API is running..." when accessing http://localhost:8080/.

REST API Security with Rust, MongoDB, and OAuth2 - test api


MongoDB Integration and User Model in Rust

To store user accounts and credentials securely, we’ll integrate MongoDB using the official MongoDB Rust driver. We'll define a User struct, set up a MongoDB connection, and prepare the database layer for our REST API.

1. MongoDB Configuration

Inside src/config/mod.rs, add the following MongoDB setup:

use mongodb::{Client, Database};
use std::env;

pub async fn connect_to_db() -> Database {
    let mongo_uri = env::var("MONGO_URI").expect("MONGO_URI must be set in .env");
    let client = Client::with_uri_str(mongo_uri)
        .await
        .expect("Failed to initialize MongoDB client");

    client.database("rust_oauth2_api")
}

🧪 This connects to your local MongoDB server and selects the rust_oauth2_api database.

2. Update main.rs to include DB Connection

In src/main.rs:

mod config;
use config::connect_to_db;

use actix_web::{web, App, HttpServer, Responder};
use dotenv::dotenv;
use std::env;

async fn index() -> impl Responder {
    "Rust OAuth2 API is running..."
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());

    let db = connect_to_db().await;

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(db.clone()))
            .route("/", web::get().to(index))
    })
    .bind(("127.0.0.1", port.parse().unwrap()))?
    .run()
    .await
}

This injects the MongoDB database instance into the application state so it can be shared across handlers.

3. Define the User Model

Inside src/models/user.rs:

use serde::{Deserialize, Serialize};
use mongodb::bson::oid::ObjectId;

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    pub username: String,
    pub email: String,
    pub password_hash: String,
}

Then, expose it in src/models/mod.rs:

pub mod user;

4. Create Users Collection Accessor (Optional Helper)

You can add this to a helper module like src/utils/db.rs (optional):

use mongodb::{Database, Collection};
use crate::models::user::User;

pub fn get_user_collection(db: &Database) -> Collection<User> {
    db.collection::<User>("users")
}

✅ At this point, you have:

  • A working connection to MongoDB
  • A User model struct with BSON support
  • Access to the user's collection


User Registration Handler with Argon2 Password Hashing

In this section, you'll:

  • Create a route to handle user registration.
  • Hash passwords securely with Argon2.
  • Store the user in MongoDB.

1. Add Password Hashing Utility

Create src/utils/hash.rs and add:

use argon2::{
    Argon2,
    password_hash::{ PasswordHasher, SaltString, rand_core::OsRng, PasswordHash, PasswordVerifier },
};

pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();

    let password_hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
    Ok(password_hash)
}

pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
    let parsed_hash = PasswordHash::new(hash)?;
    Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
}

Then register the module in src/utils/mod.rs:

pub mod db;
pub mod hash;

2. Create the Registration Handler

In src/handlers/auth.rs:

use actix_web::{ web, HttpResponse };
use mongodb::Database;
use crate::models::user::User;
use crate::utils::hash::hash_password;
use mongodb::bson::doc;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
    pub username: String,
    pub email: String,
    pub password: String,
}

pub async fn register_user(
    db: web::Data<Database>,
    form: web::Json<RegisterRequest>
) -> HttpResponse {
    let collection = db.collection::<User>("users");

    // Check for existing user
    if let Ok(Some(_)) = collection.find_one(doc! { "email": &form.email }).await {
        return HttpResponse::BadRequest().body("Email already in use");
    }

    // Hash password
    let password_hash = match hash_password(&form.password) {
        Ok(hash) => hash,
        Err(_) => {
            return HttpResponse::InternalServerError().body("Failed to hash password");
        }
    };

    let new_user = User {
        id: None,
        username: form.username.clone(),
        email: form.email.clone(),
        password_hash,
    };

    match collection.insert_one(new_user).await {
        Ok(_) => HttpResponse::Ok().body("User registered successfully"),
        Err(_) => HttpResponse::InternalServerError().body("Failed to register user"),
    }
}

3. Register the Route

In src/handlers/mod.rs:

pub mod auth;

Then in main.rs, register the handler:

mod models;
mod handlers;
mod utils;
mod config;
use config::connect_to_db;

use actix_web::{ web, App, HttpServer, Responder };
use dotenv::dotenv;
use handlers::auth::register_user;

async fn index() -> impl Responder {
    "Rust OAuth2 API is running..."
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();

    let db = connect_to_db().await;

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(db.clone()))
            .route("/", web::get().to(index))
            .route("/register", web::post().to(register_user))
    })
        .bind("127.0.0.1:8080")?
        .run().await
}

4. Test the Endpoint

Use Postman or curl:

curl -X POST http://localhost:8080/register \
  -H "Content-Type: application/json" \
  -d '{"username":"djamware","email":"[email protected]","password":"mypassword"}'

✅ Now you have:

  • Secure password hashing with Argon2
  • A working /register endpoint
  • Duplicate email protection


User Login and JWT Token Generation

This section will:

  • Verify user credentials
  • Compare password hash using Argon2
  • Generate a JWT token for authenticated sessions

1. Add JWT Dependencies

In Cargo.toml, add if not exist:

jsonwebtoken = "9"
chrono = "0.4"

2. Create JWT Utility

Create src/utils/jwt.rs:

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

const SECRET: &[u8] = b"your_secret_key_change_me";

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

pub fn create_jwt(user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
    let expiration = Utc::now()
        .checked_add_signed(Duration::hours(24))
        .expect("valid timestamp")
        .timestamp() as usize;

    let claims = Claims {
        sub: user_id.to_owned(),
        exp: expiration,
    };

    encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET))
}

pub fn verify_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(SECRET),
        &Validation::new(Algorithm::HS256),
    )?;

    Ok(token_data.claims)
}

And in src/utils/mod.rs:

3. Create Login Handler

Update src/handlers/auth.rs:

use actix_web::{web, HttpResponse};
use mongodb::{Database, bson::doc};
use serde::Deserialize;

use crate::models::user::User;
use crate::utils::hash::verify_password;
use crate::utils::jwt::create_jwt;

#[derive(Debug, Deserialize)]
pub struct LoginRequest {
    pub email: String,
    pub password: String,
}

pub async fn login_user(db: web::Data<Database>, form: web::Json<LoginRequest>) -> HttpResponse {
    let collection = db.collection::<User>("users");

    let user = match collection.find_one(doc! { "email": &form.email }).await {
        Ok(Some(user)) => user,
        _ => {
            return HttpResponse::Unauthorized().body("Invalid email or password");
        }
    };

    // Verify password
    match verify_password(&user.password_hash, &form.password) {
        Ok(true) => {
            match create_jwt(&user.email) {
                Ok(token) => HttpResponse::Ok().json(serde_json::json!({ "token": token })),
                Err(_) => HttpResponse::InternalServerError().body("Token generation failed"),
            }
        }
        _ => HttpResponse::Unauthorized().body("Invalid email or password"),
    }
}

4. Add Login Route

In main.rs:

use handlers::auth::{register_user, login_user};

.route("/register", web::post().to(register_user))
.route("/login", web::post().to(login_user))

5. Test with Postman or cURL

curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "mypassword"}'

✅ If successful, you’ll receive a JWT token in the response.

{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbkBkamFtd2FyZS5jb20iLCJleHAiOjE3NDg0OTkxMDB9.4PM-z05TX0N7IQ4LLZxPFz0qWdwAgOagIpdMaJgKXTM"}


Protecting Routes using the JWT (Authorization Middleware)

Now let’s protect your routes using JWT by building a custom middleware in Actix-web that:

  • Checks for the Authorization header
  • Verifies the JWT
  • Adds user claims to the request extensions (so handlers can use them)

1. Create JWT Middleware

Create src/middleware/jwt_auth.rs:

use actix_web::{
    dev::{ Service, ServiceRequest, ServiceResponse, Transform },
    Error,
    HttpMessage,
    HttpResponse,
    body::EitherBody,
};
use futures_util::future::{ ok, Ready, LocalBoxFuture };
use std::rc::Rc;

pub struct AuthMiddleware;

impl<S, B> Transform<S, ServiceRequest>
    for AuthMiddleware
    where
        S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
        B: 'static
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Transform = AuthMiddlewareMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(AuthMiddlewareMiddleware {
            service: Rc::new(service),
        })
    }
}

pub struct AuthMiddlewareMiddleware<S> {
    service: Rc<S>,
}

impl<S, B> Service<ServiceRequest>
    for AuthMiddlewareMiddleware<S>
    where
        S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
        B: 'static
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &self,
        ctx: &mut std::task::Context<'_>
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.service.poll_ready(ctx)
    }

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let service = Rc::clone(&self.service);

        Box::pin(async move {
            let token = req
                .headers()
                .get("Authorization")
                .and_then(|h| h.to_str().ok())
                .and_then(|h| h.strip_prefix("Bearer "))
                .map(|s| s.to_string());

            if let Some(token) = token {
                match crate::utils::jwt::verify_jwt(&token) {
                    Ok(claims) => {
                        req.extensions_mut().insert(claims);
                        let res = service.call(req).await?;
                        Ok(res.map_into_left_body())
                    }
                    Err(_) => {
                        let response = req.into_response(
                            HttpResponse::Unauthorized().body("Invalid token").map_into_right_body()
                        );
                        Ok(response)
                    }
                }
            } else {
                let response = req.into_response(
                    HttpResponse::Unauthorized().body("Missing token").map_into_right_body()
                );
                Ok(response)
            }
        })
    }
}

Add it to middleware/mod.rs:

pub mod jwt_auth;

2. Register the Middleware

In your main.rs, after adding .app_data(db.clone()), update your protected routes like:

mod models;
mod handlers;
mod utils;
mod config;
mod middleware;
use config::connect_to_db;

use actix_web::{ middleware::Logger, web, App, HttpServer, Responder };
use dotenv::dotenv;
use handlers::auth::{ get_profile, login_user, register_user };
use crate::middleware::jwt_auth::AuthMiddleware;

async fn index() -> impl Responder {
    "Rust OAuth2 API is running..."
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();

    let db = connect_to_db().await;

    HttpServer::new(move || {
        App::new()
            .wrap(Logger::default())
            .app_data(web::Data::new(db.clone()))
            .route("/", web::get().to(index))
            .route("/register", web::post().to(register_user))
            .route("/login", web::post().to(login_user))
            .service(
                web
                    ::scope("/api")
                    .wrap(AuthMiddleware)
                    .route("/profile", web::get().to(get_profile))
            )
    })
        .bind("127.0.0.1:8080")?
        .run().await
}

3. Create a Protected Route Example

In handlers/auth.rs:

use actix_web::{HttpRequest, HttpResponse};
use crate::utils::jwt::Claims;

pub async fn get_profile(req: HttpRequest) -> HttpResponse {
    if let Some(claims) = req.extensions().get::<Claims>() {
        HttpResponse::Ok().json(serde_json::json!({
            "email": claims.sub,
            "message": "This is a protected route"
        }))
    } else {
        HttpResponse::Unauthorized().body("Unauthorized")
    }
}

Example Test with JWT

curl -H "Authorization: Bearer your_with_token_from_login_success" http://localhost:8080/api/profile

✅ You now have a fully working JWT-protected route!


Token Refresh Logic

1. Access Token: Short-lived JWT (e.g., 15 mins)

2. Refresh Token: Long-lived JWT (e.g., 7 days) stored in the database (or sent via secure cookie)

3. Client flow:

  •    On login: receive both access and refresh tokens
  •    When the access token expires, send the refresh token to get a new access token

1. Update Your JWT Claims and Functions

In auth.rs or utils/jwt.rs:

use jsonwebtoken::errors::Error as JwtError;

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,
    pub exp: usize,
    pub token_type: String, // "access" or "refresh"
}

pub fn create_jwt(user_id: &str, minutes: i64, token_type: &str) -> Result<String, JwtError> {
    let expiration = Utc::now()
        .checked_add_signed(Duration::minutes(minutes))
        .expect("valid timestamp")
        .timestamp();

    let claims = Claims {
        sub: user_id.to_owned(),
        exp: expiration as usize,
        token_type: token_type.to_owned(),
    };

    let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");

    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret.as_bytes()),
    )
}

2. Refresh Token Handler

In handlers/auth.rs:

use actix_web::error::ErrorUnauthorized;
use actix_web::{ post, web, Error, HttpMessage, HttpRequest, HttpResponse };
use jsonwebtoken::{ decode, Algorithm, DecodingKey, Validation };
use mongodb::Database;
use crate::models::user::User;
use crate::utils::hash::hash_password;
use mongodb::bson::doc;
use serde::{ Deserialize, Serialize };
use crate::utils::hash::verify_password;
use crate::utils::jwt::{ create_jwt, Claims };

#[derive(Deserialize)]
pub struct RefreshRequest {
    refresh_token: String,
}

#[derive(Serialize)]
pub struct TokenResponse {
    access_token: String,
}

#[post("/refresh")]
async fn refresh_token(web::Json(payload): web::Json<RefreshRequest>) -> Result<
    HttpResponse,
    Error
> {
    let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");

    let token_data = decode::<Claims>(
        &payload.refresh_token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &Validation::new(Algorithm::HS256)
    ).map_err(|_| ErrorUnauthorized("Invalid refresh token"))?;

    if token_data.claims.token_type != "refresh" {
        return Err(ErrorUnauthorized("Not a refresh token"));
    }

    let access_token = create_jwt(&token_data.claims.sub, 15, "access").map_err(|_|
        ErrorUnauthorized("Failed to create token")
    )?;

    Ok(HttpResponse::Ok().json(TokenResponse { access_token }))
}

3. Add Route

In main.rs or wherever you configure routes:

use handlers::auth::{ get_profile, login_user, refresh_token, register_user };

        App::new()
            .wrap(Logger::default())
            .app_data(web::Data::new(db.clone()))
            .route("/", web::get().to(index))
            .route("/register", web::post().to(register_user))
            .route("/login", web::post().to(login_user))
            .service(refresh_token)
            .service(
                web
                    ::scope("/api")
                    .wrap(AuthMiddleware)
                    .route("/profile", web::get().to(get_profile))
            )

4. Modify Login Request

In handlers/auth.rs update:

pub async fn login_user(db: web::Data<Database>, form: web::Json<LoginRequest>) -> HttpResponse {
    let collection = db.collection::<User>("users");

    let user = match collection.find_one(doc! { "email": &form.email }).await {
        Ok(Some(user)) => user,
        _ => {
            return HttpResponse::Unauthorized().body("Invalid email or password");
        }
    };

    // Verify password
    match verify_password(&user.password_hash, &form.password) {
        Ok(true) => {
            match create_jwt(&user.email, 5, "access") {
                Ok(token) => HttpResponse::Ok().json(serde_json::json!({ "token": token })),
                Err(_) => HttpResponse::InternalServerError().body("Token generation failed"),
            }
        }
        _ => HttpResponse::Unauthorized().body("Invalid email or password"),
    }
}

4. Frontend Flow

On the frontend:

  • If the access token expires, call /refresh with the refresh token.
  • Use the new access token to retry the request.


Persist Refresh Tokens in MongoDB

1. Extend the MongoDB User Model

Add a refresh_tokens field (or just one refresh_token if only one is allowed per user).

Updating models/user.rs

use serde::{ Deserialize, Serialize };
use mongodb::bson::oid::ObjectId;

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    #[serde(rename = "_id")]
    pub id: Option<ObjectId>,
    pub email: String,
    pub password: String,
    pub refresh_token: Option<String>,
}

2. Update Register, Login, and Refresh Token Handler

In your handlers/auth.rs:

use actix_web::error::{ ErrorInternalServerError, ErrorUnauthorized };
use actix_web::{ post, web, Error, HttpMessage, HttpRequest, HttpResponse };
use jsonwebtoken::{ decode, Algorithm, DecodingKey, Validation };
use mongodb::Database;
use crate::models::user::User;
use crate::utils::hash::hash_password;
use mongodb::bson::doc;
use serde::{ Deserialize, Serialize };
use crate::utils::hash::verify_password;
use crate::utils::jwt::{ create_jwt, Claims };

#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
    pub email: String,
    pub password: String,
}

pub async fn register_user(
    db: web::Data<Database>,
    form: web::Json<RegisterRequest>
) -> HttpResponse {
    let collection = db.collection::<User>("users");

    // Check for existing user
    if let Ok(Some(_)) = collection.find_one(doc! { "email": &form.email }).await {
        return HttpResponse::BadRequest().body("Email already in use");
    }

    // Hash password
    let password_hash = match hash_password(&form.password) {
        Ok(hash) => hash,
        Err(_) => {
            return HttpResponse::InternalServerError().body("Failed to hash password");
        }
    };

    let new_user = User {
        id: None,
        email: form.email.clone(),
        password: password_hash,
        refresh_token: None,
    };

    match collection.insert_one(new_user).await {
        Ok(_) => HttpResponse::Ok().body("User registered successfully"),
        Err(_) => HttpResponse::InternalServerError().body("Failed to register user"),
    }
}

#[derive(Debug, Deserialize)]
pub struct LoginRequest {
    pub email: String,
    pub password: String,
}

#[post("/login")]
async fn login(
    db: web::Data<Database>,
    credentials: web::Json<LoginRequest>
) -> Result<HttpResponse, Error> {
    let collection = db.collection::<User>("users");

    let user = collection
        .find_one(doc! { "email": &credentials.email }).await
        .map_err(|e| ErrorInternalServerError(format!("Database error: {}", e)))?
        .ok_or_else(|| ErrorUnauthorized("Invalid credentials"))?;

    // validate password (use argon2 verification)
    if
        !verify_password(&credentials.password, &user.password).map_err(|_|
            ErrorUnauthorized("Invalid credentials")
        )?
    {
        return Err(ErrorUnauthorized("Invalid credentials"));
    }

    let access_token = create_jwt(&user.email, 15, "access").map_err(|e|
        ErrorInternalServerError(format!("Token generation error: {}", e))
    )?;

    let new_refresh_token = create_jwt(&user.email, 60 * 24 * 7, "refresh").map_err(|e|
        ErrorInternalServerError(format!("Token creation failed: {}", e))
    )?;

    // Save refresh token
    collection
        .update_one(
            doc! { "email": &user.email },
            doc! { "$set": { "refresh_token": &new_refresh_token } }
        ).await
        .map_err(|e| ErrorInternalServerError(format!("Database update failed: {}", e)))?;

    Ok(
        HttpResponse::Ok().json(TokenResponse {
            access_token,
            refresh_token: new_refresh_token,
        })
    )
}

pub async fn get_profile(req: HttpRequest) -> HttpResponse {
    if let Some(claims) = req.extensions().get::<Claims>() {
        HttpResponse::Ok().json(
            serde_json::json!({
            "email": claims.sub,
            "message": "This is a protected route"
        })
        )
    } else {
        HttpResponse::Unauthorized().body("Unauthorized")
    }
}

#[derive(Deserialize)]
pub struct RefreshRequest {
    pub refresh_token: String,
}

#[derive(Serialize)]
pub struct TokenResponse {
    access_token: String,
    refresh_token: String,
}

#[post("/refresh")]
async fn refresh_token(
    db: web::Data<Database>,
    payload: web::Json<RefreshRequest>
) -> Result<HttpResponse, Error> {
    let secret = std::env::var("JWT_SECRET").unwrap();

    let claims = decode::<Claims>(
        &payload.refresh_token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &Validation::new(Algorithm::HS256)
    ).map_err(|_| ErrorUnauthorized("Invalid refresh token"))?.claims;

    if claims.token_type != "refresh" {
        return Err(ErrorUnauthorized("Not a refresh token"));
    }

    let collection = db.collection::<User>("users");
    let user = collection
        .find_one(doc! { "email": &claims.sub }).await
        .map_err(|e| ErrorInternalServerError(format!("Database error: {}", e)))?
        .ok_or_else(|| ErrorUnauthorized("Invalid credentials"))?;

    if user.refresh_token.as_deref() != Some(&payload.refresh_token) {
        return Err(ErrorUnauthorized("Refresh token mismatch"));
    }

    let new_access_token = create_jwt(&user.email, 15, "access").map_err(|e|
        ErrorInternalServerError(format!("Token creation failed: {}", e))
    )?;

    let new_refresh_token = create_jwt(&user.email, 60 * 24 * 7, "refresh").map_err(|e|
        ErrorInternalServerError(format!("Token creation failed: {}", e))
    )?;

    // Update stored refresh token
    collection
        .update_one(
            doc! { "email": &user.email },
            doc! { "$set": { "refresh_token": &new_refresh_token } }
        ).await
        .map_err(|e| ErrorInternalServerError(format!("Database update failed: {}", e)))?;

    Ok(
        HttpResponse::Ok().json(TokenResponse {
            access_token: new_access_token,
            refresh_token: new_refresh_token,
        })
    )
}

3. Optional Logout Handler

Add this logout handler in your handlers/auth.rs:

use actix_web::{HttpRequest, Error};
use actix_web::http::header::AUTHORIZATION;
use crate::utils::jwt::extract_email_from_jwt;

#[post("/logout")]
async fn logout(db: web::Data<Database>, req: HttpRequest) -> Result<HttpResponse, Error> {
    let user_email = get_email_from_request(&req)?;

    let collection = db.collection::<User>("users");
    collection
        .update_one(doc! { "email": user_email }, doc! { "$unset": { "refresh_token": "" } }).await
        .map_err(|e| ErrorInternalServerError(format!("Database update failed: {}", e)))?;

    Ok(HttpResponse::Ok().body("Logged out"))
}

fn get_email_from_request(req: &HttpRequest) -> Result<String, Error> {
    let token = req
        .headers()
        .get(AUTHORIZATION)
        .and_then(|h| h.to_str().ok())
        .and_then(|auth_header| auth_header.strip_prefix("Bearer "))
        .ok_or_else(||
            actix_web::error::ErrorUnauthorized("Missing or invalid Authorization header")
        )?;

    extract_email_from_jwt(token)
}

4. Implementation: extract_email_from_jwt

Add this extract_email_from_jwt to utils/jwt.rs:

use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm, TokenData};
use serde::{Deserialize, Serialize};
use std::env;
use actix_web::error::ErrorUnauthorized;
use actix_web::Error;

pub fn extract_email_from_jwt(token: &str) -> Result<String, Error> {
    let secret = env::var("JWT_SECRET").map_err(|_| ErrorUnauthorized("Missing JWT secret"))?;

    let token_data: TokenData<Claims> = decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &Validation::new(Algorithm::HS256),
    )
    .map_err(|_| ErrorUnauthorized("Invalid or expired token"))?;

    Ok(token_data.claims.sub)
}

5. Register All Updated Routes

In main.rs, update all:

mod models;
mod handlers;
mod utils;
mod config;
mod middleware;
use config::connect_to_db;

use actix_web::{ middleware::Logger, web, App, HttpServer, Responder };
use dotenv::dotenv;
use handlers::auth::{ get_profile, login, refresh_token, register_user };
use crate::middleware::jwt_auth::AuthMiddleware;

async fn index() -> impl Responder {
    "Rust OAuth2 API is running..."
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();

    let db = connect_to_db().await;

    HttpServer::new(move || {
        App::new()
            .wrap(Logger::default())
            .app_data(web::Data::new(db.clone()))
            .route("/", web::get().to(index))
            .route("/register", web::post().to(register_user))
            .service(login)
            .service(refresh_token)
            .service(
                web
                    ::scope("/api")
                    .wrap(AuthMiddleware)
                    .route("/profile", web::get().to(get_profile))
            )
    })
        .bind("127.0.0.1:8080")?
        .run().await
}

Now that you've successfully extracted the email from the JWT, you're ready to:

  • Secure routes using the authenticated user's email.
  • Query MongoDB with the user email for profile, data, or permissions.
  • Refresh tokens or validate scopes (if needed).


Conclusion

In this tutorial, you built a secure REST API in Rust using Actix-web, MongoDB, and JWT-based authentication, along with optional OAuth2 support. You learned how to:

  • Set up a Rust web project with Actix-web
  • Connect to MongoDB and model user data
  • Implement user registration and login with Argon2 password hashing
  • Issue and verify JWTs for secure authentication
  • Add route protection with JWT middleware
  • Support token refresh and persist refresh tokens in MongoDB
  • Extract user information from tokens for route-level logic
  • Optionally expand with OAuth2 and your provider

This foundation gives you the flexibility and performance of Rust while ensuring a robust security model. From here, you can extend your API with role-based authorization, revoke refresh tokens on logout, and integrate third-party identity providers.

You can get the full working source code on our GitHub.

That is just the basics. If you need more deep learning about the Rust language and frameworks, you can take the following cheap course:

Thanks!