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.