Setting and Reading Session Cookies in Rust with Actix Web

webbureaucrat

webbureaucrat

Posted on July 18, 2023

Setting and Reading Session Cookies in Rust with Actix Web

In case it's not readily apparent, I've been having some trouble picking a backend stack recently, but, at the risk of jinxing it, I really think Rust is the backend for me, and Actix Web is a blindingly fast MVC framework for Rust with an elegant and functional middleware system. As with all web apps, my first challenge is persisting and managing state between requests, so let's get into user session management in Actix Web.

Starting with Hello World in Rust with Actix Web

For completeness, I'm going to start with cargo new. Feel free to skip ahead.

You can start with cargo new actix-kata.

If you enter your directory, you should find a "Hello, world" program. If youcargo run it, you should get "Hello, world" printed to the screen.

To add Actix Web to your project, run cargo add actix-web. If you cargo build or cargo run again, it should install Actix Web and all its dependencies.

Then replace src/main.rs with the following:

use actix_web;

#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
    actix_web::HttpResponse::Ok().body("Hello, world")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    actix_web::HttpServer::new( || {
    actix_web::App::new()
        .service(index)
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Getting started with actix-session

actix-session is it's own Rust crate, separate from actix-web. It has a feature flag for cookie sessions. To use it, install it with cargo add actix-session --features cookie-session.

Our goal will be to build out a SessionMiddleware<CookieSessionStore>. We can do that with aSessionMiddlewareBuilder. To put a fine point on it, let's break it out into its own function.

use actix_session::{ SessionMiddleware };
use actix_session::storage::{ CookieSessionStore };
use actix_web;
use actix_web::cookie::{ Key };

#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
    actix_web::HttpResponse::Ok().body("Hello, world")
}

fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
    SessionMiddleware::builder(
        CookieSessionStore::default(), Key::from(&[0; 64])
    )
    .build()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    actix_web::HttpServer::new( || {
    actix_web::App::new()
        .wrap(session_middleware()) //register session middleware
        .service(index)
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

This accepts all defaults and registers the session middleware in the application.

Building out a cookie session in Actix Web

Now let's fill in some details using the provided builder methods. (Some of these are set by default, but in the interest of a good demonstration, let's make as much explicit as we can.)

Importantly, many possible configuration items are matters of security both for the application and for the user, so it is a very good idea to familiarize oneself with each item in the documentation.

use actix_session::{ SessionMiddleware, Session };
use actix_session::config::{ BrowserSession, CookieContentSecurity };
use actix_session::storage::{ CookieSessionStore };
use actix_web;
use actix_web::cookie::{ Key, SameSite };

#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
    actix_web::HttpResponse::Ok().body("Hello, world")
}

fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
    SessionMiddleware::builder(
        CookieSessionStore::default(), Key::from(&[0; 64])
    )
    .cookie_name(String::from("my-kata-cookie")) // arbitrary name
    .cookie_secure(true) // https only
    .session_lifecycle(BrowserSession::default()) // expire at end of session
    .cookie_same_site(SameSite::Strict) 
    .cookie_content_security(CookieContentSecurity::Private) // encrypt
    .cookie_http_only(true) // disallow scripts from reading
    .build()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    actix_web::HttpServer::new( || {
    actix_web::App::new()
        .wrap(session_middleware())
        .service(index)
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Reading from a cookie in Actix Web

Reading from and writing to cookies is very simple with the Sessionextractor. Just include a parameter typed as Session, and the framework will take care of the rest.

use actix_session::{ SessionMiddleware, Session };
use actix_session::config::{ BrowserSession, CookieContentSecurity };
use actix_session::storage::{ CookieSessionStore };
use actix_web::{ HttpResponse };
use actix_web::cookie::{ Key, SameSite };
use actix_web::web::{ Json };

#[actix_web::get("get_session")]
async fn get_session(session: Session) -> impl actix_web::Responder {
    match session.get::<String>("message") {
    Ok(message_option) => {
        match message_option {
        Some(message) => HttpResponse::Ok().body(message),
        None => HttpResponse::NotFound().body("Not set.")
        }
    }
    Err(_) => HttpResponse::InternalServerError().body("Session error.")
    }
}

#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
    HttpResponse::Ok().body("Hello, world")
}

fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
    SessionMiddleware::builder(
        CookieSessionStore::default(), Key::from(&[0; 64])
    )
    .cookie_name(String::from("my-kata-cookie"))
    .cookie_secure(true)
    .session_lifecycle(BrowserSession::default())
    .cookie_same_site(SameSite::Strict)
    .cookie_content_security(CookieContentSecurity::Private)
    .cookie_http_only(true)
    .build()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    actix_web::HttpServer::new( || {
    actix_web::App::new()
        .wrap(session_middleware())
        .service(index)
        .service(get_session)
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Setting the session cookie in Actix Web

We can use serde to create an endpoint to set the session. We'll also need the derive feature.

cargo add serde --features derive
Enter fullscreen mode Exit fullscreen mode

At this point, your Cargo.toml should look like this:

[package]
name = "actix-kata"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-session = { version = "0.7.2", features = ["cookie-session"] }
actix-web = "4.3.1"
serde = { version = "1.0.164", features = ["derive"] }
Enter fullscreen mode Exit fullscreen mode

With serde, we can make a deserializable struct to model our input and then extract it simply by putting it in the signature of our method. The framework will handle the rest.

Then we can insert the message using session.insert.

main.rs

use actix_session::{ SessionMiddleware, Session };
use actix_session::config::{ BrowserSession, CookieContentSecurity };
use actix_session::storage::{ CookieSessionStore };
use actix_web::{ HttpResponse };
use actix_web::cookie::{ Key, SameSite };
use actix_web::web::{ Json };
use serde;

#[derive(serde::Deserialize)]
struct CookieModel {
    message: String
}

#[actix_web::get("get_session")]
async fn get_session(session: Session) -> impl actix_web::Responder {
    match session.get::<String>("message") {
    Ok(message_option) => {
        match message_option {
        Some(message) => HttpResponse::Ok().body(message),
        None => HttpResponse::NotFound().body("Not set.")
        }
    }
    Err(_) => HttpResponse::InternalServerError().body("Error.")
    }
}

#[actix_web::get("/")]
async fn index() -> impl actix_web::Responder {
    HttpResponse::Ok().body("Hello, world")
}

async fn set_session(session: Session, model: Json<CookieModel>)
                     -> impl actix_web::Responder
{
    match session.insert("message", model.message.clone()) {
        Ok(_) => HttpResponse::Created().body("Created."),
        Err(_) => HttpResponse::InternalServerError().body("Error.")
    }
}

fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
    SessionMiddleware::builder(
        CookieSessionStore::default(), Key::from(&[0; 64])
    )
    .cookie_name(String::from("my-kata-cookie"))
    .cookie_secure(true)
    .session_lifecycle(BrowserSession::default())
    .cookie_same_site(SameSite::Strict)
    .cookie_content_security(CookieContentSecurity::Private)
    .cookie_http_only(true)
    .build()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    actix_web::HttpServer::new( || {
    actix_web::App::new()
        .wrap(session_middleware())
        .service(index)
        .service(get_session)
        .route("set_session", actix_web::web::post().to(set_session))
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}
Enter fullscreen mode Exit fullscreen mode

Wrapping up

And that's really it. Make sure you set your mime type for the /set_session request to application/json if you're testing. This has been helpful to me as I get into Actix Web middleware for the first time, so I hope it has been helpful to you.

💖 💪 🙅 🚩
webbureaucrat
webbureaucrat

Posted on July 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related