Debug School

rakesh kumar
rakesh kumar

Posted on

How to apply Persistent Login to Secure Sessions in Rust

Normally in web applications there are two types of login systems.

  1. Session Login (Normal Login)

User logs in → session cookie created → session stored in server.

Problem:

If browser closes → session expires

If server restarts → session lost
Enter fullscreen mode Exit fullscreen mode

User must login again

  1. Persistent Login (Remember Me)

Persistent login allows:

User logs in → system stores secure token in database + cookie

Even if:

browser closes

session expires

server restarts
Enter fullscreen mode Exit fullscreen mode

User can still be automatically logged in.

So the purpose of persistent login is:

Improve user experience

Reduce repeated login

Maintain security

Allow long-term authentication safely
Enter fullscreen mode Exit fullscreen mode

Add security env vars
Add security env vars: SESSION_KEY, SESSION_TTL_HOURS, SESSION_SECURE_COOKIE.

SESSION_KEY=replace-with-32-byte-random-secret
SESSION_TTL_HOURS=24
# In production behind HTTPS set true. Local HTTP dev can stay false.
SESSION_SECURE_COOKIE=false
Enter fullscreen mode Exit fullscreen mode

Read and validate those env vars into AppConfig fields:
src/config.rs
Read and validate those env vars into AppConfig fields:

session_ttl_hours
session_secure_cookie

pub session_ttl_hours: i64,
pub session_secure_cookie: bool,
Enter fullscreen mode Exit fullscreen mode
   let session_ttl_hours = env::var("SESSION_TTL_HOURS")
            .ok()
            .and_then(|v| v.parse::<i64>().ok())
            .filter(|v| *v > 0)
            .unwrap_or(24);

        // Keep local dev usable on HTTP, default secure in non-local environments.
        let inferred_local = bind_addr.contains("127.0.0.1") || bind_addr.contains("localhost");
        let session_secure_cookie = env::var("SESSION_SECURE_COOKIE")
            .ok()
            .map(|v| {
                let lower = v.trim().to_ascii_lowercase();
                lower == "1" || lower == "true" || lower == "yes" || lower == "on"
            })
            .unwrap_or(!inferred_local);

        Self {
            app_name,
            bind_addr,
            database_url,
            session_ttl_hours,
            session_secure_cookie,
        }
Enter fullscreen mode Exit fullscreen mode

full code

use std::env;

#[derive(Clone, Debug)]
pub struct AppConfig {
    pub app_name: String,
    pub bind_addr: String,
    pub database_url: String,
    pub session_ttl_hours: i64,
    pub session_secure_cookie: bool,
}

impl AppConfig {
    pub fn from_env() -> Self {
        let app_name = env::var("APP_NAME").unwrap_or_else(|_| "Rust CRUD Dashboard".to_string());
        let bind_addr = env::var("APP_BIND").unwrap_or_else(|_| "127.0.0.1:8080".to_string());
        let database_url = env::var("DATABASE_URL")
            .unwrap_or_else(|_| "mysql://root:@127.0.0.1:3306/rust_crud".to_string());
        let session_ttl_hours = env::var("SESSION_TTL_HOURS")
            .ok()
            .and_then(|v| v.parse::<i64>().ok())
            .filter(|v| *v > 0)
            .unwrap_or(24);

        // Keep local dev usable on HTTP, default secure in non-local environments.
        let inferred_local = bind_addr.contains("127.0.0.1") || bind_addr.contains("localhost");
        let session_secure_cookie = env::var("SESSION_SECURE_COOKIE")
            .ok()
            .map(|v| {
                let lower = v.trim().to_ascii_lowercase();
                lower == "1" || lower == "true" || lower == "yes" || lower == "on"
            })
            .unwrap_or(!inferred_local);

        Self {
            app_name,
            bind_addr,
            database_url,
            session_ttl_hours,
            session_secure_cookie,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Apply session middleware hardening:
Apply session middleware hardening:

custom cookie name
cookie_secure(...)
cookie_http_only(true)
session_lifecycle(PersistentSession...session_ttl...)
safer SESSION_KEY handling (no fixed default secret)
Enter fullscreen mode Exit fullscreen mode

src/main.rs

Apply session middleware hardening:

custom cookie name
cookie_secure(...)
cookie_http_only(true)
session_lifecycle(PersistentSession...session_ttl...)
safer SESSION_KEY handling (no fixed default secret)
Enter fullscreen mode Exit fullscreen mode
      if valid {
            session.renew();
Enter fullscreen mode Exit fullscreen mode
   if valid {
            session.renew();
            session
                .insert("username", user.username.clone())
                .map_err(error::ErrorInternalServerError)?;
            session
                .insert("user_id", user.id)
                .map_err(error::ErrorInternalServerError)?;
            session
                .insert("role", role.clone())
                .map_err(error::ErrorInternalServerError)?;
            println!("[login] session_set username={}", user.username);
            let response = ApiResponse {
                message: "Login successful".to_string(),
                data: AuthUser {
                    username: user.username,
                    role,
                },
            };
            return Ok(HttpResponse::Ok().json(response));
        }
Enter fullscreen mode Exit fullscreen mode

On successful login, call session.renew() before inserting session values to reduce session fixation
src/handlers/auth.rs

use actix_session::{
    config::{PersistentSession, TtlExtensionPolicy},
    storage::CookieSessionStore,
    SessionMiddleware,
};
use actix_web::{
    cookie::{time::Duration, Key, SameSite},
    web, App, HttpServer,
};
Enter fullscreen mode Exit fullscreen mode
    let session_key = std::env::var("SESSION_KEY").unwrap_or_else(|_| {
        eprintln!("[startup] SESSION_KEY is missing; generating ephemeral key for this process.");
        format!("{}{}", uuid::Uuid::new_v4(), uuid::Uuid::new_v4())
    });
    let mut session_key_bytes = session_key.into_bytes();
    if session_key_bytes.len() < 32 {
        session_key_bytes.resize(32, b'0');
    }
Enter fullscreen mode Exit fullscreen mode
App::new()
            .app_data(state.clone())
            .wrap(cors)
            .wrap(
                SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
                    .cookie_name("rust_crud_session".to_string())
                    .cookie_secure(cfg.session_secure_cookie)
                    .cookie_http_only(true)
                    .cookie_same_site(SameSite::Lax)
                    .session_lifecycle(
                        PersistentSession::default()
                            .session_ttl(Duration::hours(cfg.session_ttl_hours))
                            .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest),
                    )
                    .build(),
            )
Enter fullscreen mode Exit fullscreen mode

full code

mod config;
mod db;
mod handlers;
mod layout;
mod middleware;
mod models;

use actix_cors::Cors;
use actix_files::Files;
use actix_session::{
    config::{PersistentSession, TtlExtensionPolicy},
    storage::CookieSessionStore,
    SessionMiddleware,
};
use actix_web::{
    cookie::{time::Duration, Key, SameSite},
    web, App, HttpServer,
};
use config::AppConfig;
use db::{init_pool, normalize_brand_model_rows, normalize_shop_rows, state_data, AppState};
use dotenvy::dotenv;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    std::fs::create_dir_all("uploads/brand_models")?;
    std::fs::create_dir_all("uploads/insurance")?;
    std::fs::create_dir_all("uploads/pollution")?;
    std::fs::create_dir_all("uploads/rc_document")?;
    let cfg = AppConfig::from_env();
    let pool = init_pool(&cfg.database_url)
        .await
        .expect("Database connection failed");
    if let Err(err) = normalize_brand_model_rows(&pool).await {
        eprintln!("[startup] normalize_brand_model_rows failed: {err}");
    }
    if let Err(err) = normalize_shop_rows(&pool).await {
        eprintln!("[startup] normalize_shop_rows failed: {err}");
    }

    let state = state_data(AppState {
        pool,
        app_name: cfg.app_name.clone(),
    });

    let session_key = std::env::var("SESSION_KEY").unwrap_or_else(|_| {
        eprintln!("[startup] SESSION_KEY is missing; generating ephemeral key for this process.");
        format!("{}{}", uuid::Uuid::new_v4(), uuid::Uuid::new_v4())
    });
    let mut session_key_bytes = session_key.into_bytes();
    if session_key_bytes.len() < 32 {
        session_key_bytes.resize(32, b'0');
    }
    let key = Key::derive_from(&session_key_bytes);

    HttpServer::new(move || {
        let cors = Cors::default()
            .allowed_origin("http://localhost:5173")
            .allowed_origin("http://127.0.0.1:5173")
            .allowed_origin("http://localhost:5174")
            .allowed_origin("http://127.0.0.1:5174")
            .allowed_origin("http://localhost:8080")
            .allowed_origin("http://127.0.0.1:8080")
            .allowed_origin("http://localhost:8081")
            .allowed_origin("http://127.0.0.1:8081")
            .allow_any_header()
            .allow_any_method()
            .supports_credentials();

        App::new()
            .app_data(state.clone())
            .wrap(cors)
            .wrap(
                SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
                    .cookie_name("rust_crud_session".to_string())
                    .cookie_secure(cfg.session_secure_cookie)
                    .cookie_http_only(true)
                    .cookie_same_site(SameSite::Lax)
                    .session_lifecycle(
                        PersistentSession::default()
                            .session_ttl(Duration::hours(cfg.session_ttl_hours))
                            .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest),
                    )
                    .build(),
            )
            .service(
                web::scope("/api")
                    .route("/login", web::post().to(handlers::login))
                    .route("/logout", web::post().to(handlers::logout))
                    .route("/home/vehicles", web::get().to(handlers::list_home_vehicle_cards_handler))
                    .service(
                        web::scope("")
                            .wrap(middleware::AuthMiddleware)
                            .route("/me", web::get().to(handlers::me))
                            .route("/dashboard", web::get().to(handlers::dashboard))
                            .service(
                                web::scope("")
                                    .wrap(middleware::require_roles(&["user"]))
                                    .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))
                                    .route("/vehicles", web::get().to(handlers::list_vehicles_handler))
                                    .route("/vehicles", web::post().to(handlers::create_vehicle_handler))
                                    .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),
                                    )
                                    .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))
                                    .route("/bookings/offline", web::post().to(handlers::save_offline_booking_handler))
                                    .route("/brand-models", web::get().to(handlers::list_brand_models_handler))
                                    .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),
                                    )
                                    .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)),
                            ),
                    ),
            )
            .service(Files::new("/uploads", "./uploads"))
    })
    .bind(cfg.bind_addr)?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  let session_ttl_hours = env::var("SESSION_TTL_HOURS")
            .ok()
            .and_then(|v| v.parse::<i64>().ok())
            .filter(|v| *v > 0)
            .unwrap_or(24);
Enter fullscreen mode Exit fullscreen mode

alternative way

Same Logic in Beginner Style (Long Version)

This is the same code written in a more beginner friendly style.

use std::env;

fn main() {

    let value = env::var("SESSION_TTL_HOURS");

    let session_ttl_hours = match value {

        Ok(v) => {
            match v.parse::<i64>() {
                Ok(num) if num > 0 => num,
                _ => 24
            }
        }

        Err(_) => 24
    };

    println!("Session TTL = {}", session_ttl_hours);

}
Enter fullscreen mode Exit fullscreen mode

Output is the same.

Top comments (0)