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)?;
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
6️⃣ Step 1: Create RequireLogin Middleware
Create file:
src/middleware/require_login.rs
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 })
}
}
7️⃣ Step 2: Register Middleware in main.rs
Import it:
mod middleware;
use middleware::require_login::RequireLogin;
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))
)
)
8️⃣ What Happens Internally?
If user is not logged in:
{
"error": "Login required"
}
If logged in:
Request proceeds to handler normally.
9️⃣ Cleaner Handlers Now
Before middleware:
let user_id = ensure_login_user_id(&session)?;
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)
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))
)
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"));
}
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
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
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
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 })
}
}
2) Wire it in main.rs (your project structure)
In your main.rs you already have:
mod db;
mod handlers;
mod layout;
mod models;
Add:
mod middleware;
use middleware::require_login::RequireLogin;
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)),
),
)
What to remove from handlers now
✅ Remove this line from all protected handlers
ensure_login_user_id(&session)?;
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))
)
)
Top comments (0)