Home > Enterprise >  How do you create an actix-web HttpServer with session-based authentication?
How do you create an actix-web HttpServer with session-based authentication?

Time:09-27

I'm working on an internal API with which approved users can read from and insert into a database. My intention is for this program to run on our local network, and for multiple users or applications to be able to access it.

In its current state it functions as long as the user is running a local instance of the client and does all their work under localhost. However, when the same user attempts to log in from their IP address, the session is not stored and nothing can be accessed. The result is the same when attempting to connect from another computer on the network to a computer running the client.

Before commenting or answering, please be aware that this is my first time implementing authentication. Any mistakes or egregious errors on my part are simply out of ignorance.

My Cargo.toml file includes the following dependencies:

actix-session = { version = "0.7.1", features = ["cookie-session"] }
actix-web = "^4"
argon2 = "0.4.1"
rand_core = "0.6.3"
reqwest = "0.11.11"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
sqlx = { version = "0.6.1", features = ["runtime-actix-rustls", "mysql", "macros"] }

Here are the contents of main.rs:

use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::web::{get, post, Data, Path};
use actix_web::{HttpResponse, Responder};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let secret_key = Key::generate();

    // Load or create a new config file.
    // let settings = ...

    // Create a connection to the database.
    let pool = sqlx::mysql::MySqlPoolOptions::new()
        .connect(&format!(
            "mysql://{}:{}@{}:{}/mydb",
            env!("DB_USER"),
            env!("DB_PASS"),
            env!("DB_HOST"),
            env!("DB_PORT"),
        ))
        .await
        .unwrap();

    println!(
        "Application listening on {}:{}",
        settings.host,
        settings.port,
    );

    // Instantiate the application and add routes for each handler.
    actix_web::HttpServer::new(move || {
        let logger = actix_web::middleware::Logger::default();
        actix_web::App::new()
            .wrap(SessionMiddleware::new(
                CookieSessionStore::default(),
                secret_key.clone(),
            ))
            .wrap(logger)
            .app_data(Data::new(pool.clone()))
            /*
                Routes that return all rows from a database table.
            */
            /*
                Routes that return a webpage.
            */
            .route("/new", get().to(new))
            .route("/login", get().to(login))
            .route("/register", get().to(register))
            /*
                Routes that deal with authentication.
            */
            .route("/register", post().to(register_user))
            .route("/login", post().to(login_user))
            .route("/logout", get().to(logout_user))
            /*
                Routes that handle POST requests.
            */
    })
    .bind(format!("{}:{}", settings.host, settings.port))?
    .run()
    .await
}

The code involving authentication is as follows:

use crate::model::User;
use actix_session::Session;
use actix_web::web::{Data, Form};
use actix_web::{error::ErrorUnauthorized, HttpResponse};
use argon2::password_hash::{rand_core::OsRng, PasswordHasher, SaltString};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use sqlx::{MySql, Pool};

#[derive(serde::Serialize)]
pub struct SessionDetails {
    user_id: u32,
}

#[derive(Debug, sqlx::FromRow)]
pub struct AuthorizedUser {
    pub id: u32,
    pub username: String,
    pub password_hash: String,
    pub approved: bool,
}

pub fn check_auth(session: &Session) -> Result<u32, actix_web::Error> {
    match session.get::<u32>("user_id").unwrap() {
        Some(user_id) => Ok(user_id),
        None => Err(ErrorUnauthorized("User not logged in.")),
    }
}

pub async fn register_user(
    data: Form<User>,
    pool: Data<Pool<MySql>>,
) -> Result<String, Box<dyn std::error::Error>> {
    let data = data.into_inner();
    let salt = SaltString::generate(&mut OsRng);

    let argon2 = Argon2::default();

    let password_hash = argon2
        .hash_password(data.password.as_bytes(), &salt)
        .unwrap()
        .to_string();

    // Use to verify.
    // let parsed_hash = PasswordHash::new(&hash).unwrap();

    const INSERT_QUERY: &str =
        "INSERT INTO users (username, password_hash) VALUES (?, ?) RETURNING id;";

    let fetch_one: Result<(u32,), sqlx::Error> = sqlx::query_as(INSERT_QUERY)
        .bind(data.username)
        .bind(password_hash)
        .fetch_one(&mut pool.acquire().await.unwrap())
        .await;

    match fetch_one {
        Ok((user_id,)) => Ok(user_id.to_string()),
        Err(err) => Err(Box::new(err)),
    }
}

pub async fn login_user(
    session: Session,
    data: Form<User>,
    pool: Data<Pool<MySql>>,
) -> Result<HttpResponse, Box<dyn std::error::Error>> {
    let data = data.into_inner();
    let fetched_user: AuthorizedUser = match sqlx::query_as(
        "SELECT id, username, password_hash, approved FROM users WHERE username = ?;",
    )
    .bind(data.username)
    .fetch_one(&mut pool.acquire().await?)
    .await
    {
        Ok(fetched_user) => fetched_user,
        Err(e) => return Ok(HttpResponse::NotFound().body(format!("{e:?}"))),
    };

    let parsed_hash = PasswordHash::new(&fetched_user.password_hash).unwrap();

    match Argon2::default().verify_password(&data.password.as_bytes(), &parsed_hash) {
        Ok(_) => {
            if !fetched_user.approved {
                return Ok(
                    HttpResponse::Unauthorized().body("This account has not yet been approved.")
                );
            }

            session.insert("user_id", &fetched_user.id)?;
            session.renew();

            Ok(HttpResponse::Ok().json(SessionDetails {
                user_id: fetched_user.id,
            }))
        }
        Err(_) => Ok(HttpResponse::Unauthorized().body("Incorrect password.")),
    }
}

pub async fn logout_user(session: Session) -> HttpResponse {
    if check_auth(&session).is_err() {
        return HttpResponse::NotFound().body("No user logged in.");
    }

    session.purge();
    HttpResponse::SeeOther()
        .append_header(("Location", "/login"))
        .body(format!("User logged out successfully."))
}

I've set my client up to run with host 0.0.0.0 on port 80, but with the little networking knowledge I have that's the best I could think to do — I'm lost here. Any help would be greatly appreciated.

CodePudding user response:

As it turns out, the cookie was not being transmitted because our local network is not using https.

Changing this...

.wrap(SessionMiddleware::new(
    CookieSessionStore::default(),
    secret_key.clone(),
))

to the following...

.wrap(
    SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
        .cookie_secure(false)
        .build(),
)

solves the issue.

  • Related