Building a secure authentication system is essential for modern web applications. Rust, known for its safety and performance, combined with the Actix-web framework, offers a powerful solution for creating reliable authentication mechanisms. This step-by-step guide walks you through the process of developing an authentication system using Rust and Actix-web.

Prerequisites

  • Basic knowledge of Rust programming language
  • Rust installed on your system (version 1.65 or higher recommended)
  • Understanding of web frameworks and HTTP protocols
  • Experience with databases (PostgreSQL or SQLite recommended)

Setting Up Your Rust Project

Create a new Rust project using Cargo:

cargo new rust-auth-actix
cd rust-auth-actix

Add dependencies in Cargo.toml:

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jsonwebtoken = "8"
argon2 = "0.3"
dotenv = "0.15"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres"] }
tokio = { version = "1", features = ["full"] }

Database Setup

Choose a database system. For this guide, we'll use PostgreSQL. Create a database and a users table:

CREATE DATABASE auth_db;

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL
);

Implementing User Registration

Create a new module for user registration logic. Use Argon2 for password hashing and SQLx for database interaction.

use argon2::{self, Config};
use sqlx::PgPool;
use serde::Deserialize;

#[derive(Deserialize)]
struct RegisterInfo {
  username: String,
  password: String,
}

pub async fn register_user(
  pool: &PgPool,
  info: RegisterInfo,
) -> Result<(), sqlx::Error> {
  let password_hash = hash_password(&info.password)?;
  sqlx::query!(
    "INSERT INTO users (username, password_hash) VALUES ($1, $2)",
    info.username,
    password_hash
  )
  .execute(pool)
  .await?;
  Ok(())
}

fn hash_password(password: &str) -> Result {
  let salt = b"randomsalt";
  let config = Config::default();
  argon2::hash_encoded(password.as_bytes(), salt, &config)
}

Implementing User Login

Verify user credentials and generate JWT tokens for authenticated sessions.

use jsonwebtoken::{encode, Header, EncodingKey};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Claims {
  sub: String,
  exp: usize,
}

pub async fn login_user(
  pool: &PgPool,
  username: String,
  password: String,
) -> Result {
  let user = sqlx::query!(
    "SELECT password_hash FROM users WHERE username = $1",
    username
  )
  .fetch_one(pool)
  .await
  .map_err(|_| "User not found".to_string())?;

  if verify_password(&password, &user.password_hash)? {
    let token = create_jwt(&username)?;
    Ok(token)
  } else {
    Err("Invalid credentials".to_string())
  }
}

fn verify_password(password: &str, hash: &str) -> Result {
  argon2::verify_encoded(hash, password.as_bytes())
}

fn create_jwt(username: &str) -> Result {
  let expiration = chrono::Utc::now()
    .checked_add_signed(chrono::Duration::hours(24))
    .expect("valid timestamp")
    .timestamp() as usize;

  let claims = Claims {
    sub: username.to_owned(),
    exp: expiration,
  };

  encode(&Header::default(), &claims, &EncodingKey::from_secret(b"secret"))
}

Building the Actix-web Server

Set up your main server file with routes for registration and login.

use actix_web::{App, HttpServer, web, HttpResponse, Responder};
use sqlx::PgPool;
use dotenv::dotenv;
use std::env;

mod auth;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
  dotenv().ok();
  let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
  let pool = PgPool::connect(&database_url).await.expect("Failed to connect to database");

  HttpServer::new(move || {
    App::new()
      .app_data(web::Data::new(pool.clone()))
      .service(web::resource("/register").route(web::post().to(auth::register_user)))
      .service(web::resource("/login").route(web::post().to(auth::login_handler)))
  })
  .bind("127.0.0.1:8080")?
  .run()
  .await
}

async fn login_handler(
  pool: web::Data,
  info: web::Json,
) -> impl Responder {
  match auth::login_user(&pool, info.username.clone(), info.password.clone()).await {
    Ok(token) => HttpResponse::Ok().json(serde_json::json!({ "token": token })),
    Err(e) => HttpResponse::Unauthorized().body(e),
  }
}

Securing Endpoints with Middleware

Implement middleware to verify JWT tokens for protected routes.

use actix_web::{dev::ServiceRequest, Error, HttpMessage};
use actix_web_httpauth::middleware::HttpAuthentication;
use jsonwebtoken::{decode, Validation, DecodingKey};
use serde::{Deserialize};

#[derive(Deserialize)]
struct Claims {
  sub: String,
  exp: usize,
}

async fn validator(
  req: ServiceRequest,
  credentials: actix_web_httpauth::extractors::bearer::BearerAuth,
) -> Result {
  let token = credentials.token();
  let decoding_key = DecodingKey::from_secret(b"secret");
  let validation = Validation::default();

  decode::(token, &decoding_key, &validation)
    .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))?;

  Ok(req)
}

fn auth_middleware() -> HttpAuthentication _> {
  HttpAuthentication::bearer(validator)
}

Conclusion

By following these steps, you can build a robust authentication system with Rust and Actix-web. Remember to keep your secret keys secure and consider implementing additional security measures such as refresh tokens and multi-factor authentication for enhanced security.