Debug School

rakesh kumar
rakesh kumar

Posted on

Middleware-Based Route Protection in Actix-Web (Rust) – Production Ready Implementation Guide

Introduction
Middleware-Based Route Protection
What is Middleware in Actix-Web?
Why Use Middleware for Authentication
When Should You Use Middleware?
Architecture Overview
Why Middleware Order Matters
Advanced: Protect Only Some Routes
Add Role-Based Middleware (Advanced)
Security Benefits
Common Developer Mistakes
Production Best Practices

Practical coding example

Introduction

In small projects, we protect routes by checking session inside every handler:

let user_id = ensure_login_user_id(&session)?;
Enter fullscreen mode Exit fullscreen mode

But in large applications with 50+ routes, this becomes messy and repetitive.

The professional solution is:

Middleware-Based Route Protection

Middleware automatically blocks unauthenticated users before they reach the handler.

2️⃣ What is Middleware in Actix-Web?

Middleware is a layer that runs:

Before the request reaches the handler

After the handler returns a response

Think of it like a security checkpoint before entering a building.

3️⃣Why Use Middleware for Authentication?
Without Middleware:

Every handler repeats session check

Risk of forgetting to protect a route

Code duplication

With Middleware:

Centralized protection

Cleaner handlers

More scalable

4️⃣ When Should You Use Middleware?

Use middleware when:

You have multiple protected routes

You want global authentication rules

You want consistent 401 responses

You want scalable architecture

5️⃣ Architecture Overview

Client Request
      ↓
CORS Middleware
      ↓
Session Middleware
      ↓
RequireLogin Middleware   ← 🔐 Authentication Check
      ↓
Route Handler
      ↓
Response
Enter fullscreen mode Exit fullscreen mode

6️⃣ Step 1: Create RequireLogin Middleware

Create file:

src/middleware/require_login.rs
Enter fullscreen mode Exit fullscreen mode

Full Middleware Code

use actix_service::{Service, Transform};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    Error, error,
};
use futures_util::future::{ready, LocalBoxFuture, Ready};
use actix_session::SessionExt;

pub struct RequireLogin;

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

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(RequireLoginMiddleware { service }))
    }
}

pub struct RequireLoginMiddleware<S> {
    service: S,
}

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

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let session = req.get_session();

        let is_logged_in = session
            .get::<i64>("user_id")
            .ok()
            .flatten()
            .is_some();

        if !is_logged_in {
            return Box::pin(async {
                Err(error::ErrorUnauthorized("Login required"))
            });
        }

        let fut = self.service.call(req);
        Box::pin(async move { fut.await })
    }
}
Enter fullscreen mode Exit fullscreen mode

7️⃣ Step 2: Register Middleware in main.rs

Import it:

mod middleware;
use middleware::require_login::RequireLogin;
Enter fullscreen mode Exit fullscreen mode

Apply it only to protected routes:

.service(
    web::scope("/api")
        .route("/login", web::post().to(handlers::login))
        .route("/register", web::post().to(handlers::register))
        .service(
            web::scope("")
                .wrap(RequireLogin)
                .route("/dashboard", web::get().to(handlers::dashboard))
                .route("/shops", web::get().to(handlers::list_shops_handler))
        )
)
Enter fullscreen mode Exit fullscreen mode

8️⃣ What Happens Internally?

If user is not logged in:

{
  "error": "Login required"
}
Enter fullscreen mode Exit fullscreen mode

If logged in:

Request proceeds to handler normally.

9️⃣ Cleaner Handlers Now

Before middleware:

let user_id = ensure_login_user_id(&session)?;
Enter fullscreen mode Exit fullscreen mode

After middleware:

// No login check needed
pub async fn dashboard(...) { }

Much cleaner and safer.

🔟 Why Middleware Order Matters

Order inside .wrap() is important.

Correct order:

.wrap(cors)
.wrap(SessionMiddleware::builder(...))
.wrap(RequireLogin)
Enter fullscreen mode Exit fullscreen mode

Session must come before RequireLogin.

1️⃣1️⃣ Advanced: Protect Only Some Routes

You can structure like this:

web::scope("/api")
    .route("/login", web::post().to(login))
    .service(
        web::scope("/admin")
            .wrap(RequireLogin)
            .route("/users", web::get().to(admin_users))
    )
Enter fullscreen mode Exit fullscreen mode

Now only /api/admin/* requires login.

1️⃣2️⃣ Add Role-Based Middleware (Advanced)

Instead of checking role in handler, create AdminMiddleware.

if role != "admin" {
    return Err(error::ErrorForbidden("Admin only"));
}
Enter fullscreen mode Exit fullscreen mode

This keeps authorization separate from business logic.

1️⃣3️⃣ Security Benefits

✔ Prevents accidental unprotected route
✔ Centralized security logic
✔ Easier auditing
✔ Clean code
✔ Enterprise-ready architecture

1️⃣4️⃣ Common Developer Mistakes

❌ Forgetting to add middleware
❌ Wrong middleware order
❌ Not enabling SessionMiddleware first
❌ Returning plain text errors instead of JSON
❌ Applying middleware to login route (causes login loop)

1️⃣5️⃣ Production Best Practices

Always use HTTPS

Enable .cookie_secure(true)

Use strong SESSION_KEY

Log unauthorized attempts

Combine with session timeout
Enter fullscreen mode Exit fullscreen mode

Practical coding example

Project Structure Overview
Before writing authentication logic, we must organize the project correctly. A clean folder structure makes middleware, handlers, and DB logic easy to scale.

src/
  main.rs
  config.rs
  models.rs
  handlers.rs
  handlers/
    auth.rs
    shop.rs
    vehicle.rs
  db.rs
  db/
    common.rs
    shop.rs
    vehicle.rs
Enter fullscreen mode Exit fullscreen mode

Add middleware module files

Create these files:


src/middleware/mod.rs

src/middleware/require_login.rs

src/middleware/mod.rs
pub mod require_login;
src/middleware/require_login.rs
Enter fullscreen mode Exit fullscreen mode

This is the same production-ready middleware pattern from the middleware guide: it reads the session and blocks if user_id is missing.

use actix_service::{Service, Transform};
use actix_session::SessionExt;
use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    error,
    Error,
};
use futures_util::future::{ready, LocalBoxFuture, Ready};

pub struct RequireLogin;

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

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(RequireLoginMiddleware { service }))
    }
}

pub struct RequireLoginMiddleware<S> {
    service: S,
}

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

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let session = req.get_session();

        let is_logged_in = session
            .get::<i64>("user_id")
            .ok()
            .flatten()
            .is_some();

        if !is_logged_in {
            return Box::pin(async { Err(error::ErrorUnauthorized("Login required")) });
        }

        let fut = self.service.call(req);
        Box::pin(async move { fut.await })
    }
}
Enter fullscreen mode Exit fullscreen mode

2) Wire it in main.rs (your project structure)

In your main.rs you already have:

mod db;
mod handlers;
mod layout;
mod models;

Enter fullscreen mode Exit fullscreen mode

Add:

mod middleware;
use middleware::require_login::RequireLogin;
Enter fullscreen mode Exit fullscreen mode

Now apply middleware main.rs

Updated .service(web::scope("/api")...) (public + protected split)
.service(
    web::scope("/api")
        // -------------------------
        // ✅ PUBLIC ROUTES
        // -------------------------
        .route("/login", web::post().to(handlers::login))
        // If you want logout to work even when session expired, keep it public
        .route("/logout", web::post().to(handlers::logout))

        // Public home cards (your React homepage list)
        .route("/home/vehicles", web::get().to(handlers::list_home_vehicle_cards_handler))

        // Optional: public brand-model listing
        .route("/brand-models", web::get().to(handlers::list_brand_models_handler))

        // -------------------------
        // 🔒 PROTECTED ROUTES (RequireLogin)
        // -------------------------
        .service(
            web::scope("")
                .wrap(RequireLogin)

                // auth state
                .route("/me", web::get().to(handlers::me))
                .route("/dashboard", web::get().to(handlers::dashboard))

                // shops (protected)
                .route("/shops", web::get().to(handlers::list_shops_handler))
                .route("/shops", web::post().to(handlers::create_shop_handler))
                .route("/shops/{id}", web::get().to(handlers::get_shop_handler))
                .route("/shops/{id}", web::put().to(handlers::update_shop_handler))
                .route("/shops/{id}", web::delete().to(handlers::delete_shop_handler))

                // vehicles (protected)
                .route("/vehicles", web::get().to(handlers::list_vehicles_handler))
                .route("/vehicles", web::post().to(handlers::create_vehicle_handler))
                .route("/vehicles/{id}", web::get().to(handlers::get_vehicle_handler))
                .route("/vehicles/{id}", web::put().to(handlers::update_vehicle_handler))
                .route("/vehicles/{id}", web::delete().to(handlers::delete_vehicle_handler))

                // uploads (protected)
                .route(
                    "/vehicles/upload-documents",
                    web::post().to(handlers::upload_vehicle_documents_handler),
                )
                .route(
                    "/vehicles/upload-partner-image",
                    web::post().to(handlers::upload_vehicle_partner_image_handler),
                )

                // bookings (protected)
                .route("/bookings/offline", web::post().to(handlers::save_offline_booking_handler))

                // brand-models write actions (protected)
                .route("/brand-models", web::post().to(handlers::create_brand_model_handler))
                .route(
                    "/brand-models/upload-images",
                    web::post().to(handlers::upload_brand_model_images_handler),
                )
                // keep your alias routes but protected
                .route(
                    "/brand-models/upload-images/",
                    web::post().to(handlers::upload_brand_model_images_handler),
                )
                .route(
                    "/brand-model/upload-images",
                    web::post().to(handlers::upload_brand_model_images_handler),
                )
                .route("/brand-models/{id}", web::get().to(handlers::get_brand_model_handler))
                .route("/brand-models/{id}", web::put().to(handlers::update_brand_model_handler))
                .route("/brand-models/{id}", web::delete().to(handlers::delete_brand_model_handler)),
        ),
)
Enter fullscreen mode Exit fullscreen mode

What to remove from handlers now
✅ Remove this line from all protected handlers

ensure_login_user_id(&session)?;
Enter fullscreen mode Exit fullscreen mode

Protect only some areas (admin scope example)

If you want /api/admin/* protected but other /api/* routes public, wrap only that scope (same pattern as the guide).

.service(
    web::scope("/api")
        .route("/login", web::post().to(handlers::login))
        .service(
            web::scope("/admin")
                .wrap(RequireLogin)
                .route("/users", web::get().to(handlers::admin_users))
        )
)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)