Master essential security practices for building secure web applications with Rust. Learn about authentication, authorization, input validation, and secure communication patterns. 🚀
Authentication 🔒
use argon2::{self, Config};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc, Duration};
// Password Hashing
pub struct PasswordHasher {
config: Config<'static>,
}
impl PasswordHasher {
pub fn new() -> Self {
let config = Config::default();
Self { config }
}
pub fn hash_password(
&self,
password: &str
) -> Result<String, argon2::Error> {
let salt = Uuid::new_v4().as_bytes();
argon2::hash_encoded(
password.as_bytes(),
salt,
&self.config
)
}
pub fn verify_password(
&self,
hash: &str,
password: &str
) -> Result<bool, argon2::Error> {
argon2::verify_encoded(hash, password.as_bytes())
}
}
// JWT Implementation
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
iat: usize,
}
pub struct JwtManager {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
}
impl JwtManager {
pub fn new(secret: &[u8]) -> Self {
Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
}
}
pub fn create_token(
&self,
user_id: &str,
duration: Duration
) -> Result<String, jsonwebtoken::errors::Error> {
let now = Utc::now();
let exp = (now + duration).timestamp() as usize;
let claims = Claims {
sub: user_id.to_string(),
exp,
iat: now.timestamp() as usize,
};
encode(
&Header::default(),
&claims,
&self.encoding_key
)
}
pub fn verify_token(
&self,
token: &str
) -> Result<Claims, jsonwebtoken::errors::Error> {
decode::<Claims>(
token,
&self.decoding_key,
&Validation::default()
).map(|data| data.claims)
}
}
// Session Management
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
id: String,
user_id: String,
expires_at: DateTime<Utc>,
}
impl Session {
pub fn new(
user_id: String,
duration: Duration
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
user_id,
expires_at: Utc::now() + duration,
}
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
}
pub struct SessionStore {
redis: redis::Client,
}
impl SessionStore {
pub async fn new(redis_url: &str) -> Result<Self, redis::RedisError> {
let redis = redis::Client::open(redis_url)?;
Ok(Self { redis })
}
pub async fn store_session(
&self,
session: &Session
) -> Result<(), redis::RedisError> {
let mut conn = self.redis.get_async_connection().await?;
let key = format!("session:{}", session.id);
let value = serde_json::to_string(session).unwrap();
redis::cmd("SET")
.arg(&key)
.arg(value)
.arg("EX")
.arg(session.expires_at.timestamp() - Utc::now().timestamp())
.query_async(&mut conn)
.await
}
pub async fn get_session(
&self,
session_id: &str
) -> Result<Option<Session>, redis::RedisError> {
let mut conn = self.redis.get_async_connection().await?;
let key = format!("session:{}", session_id);
let value: Option<String> = redis::cmd("GET")
.arg(&key)
.query_async(&mut conn)
.await?;
Ok(value
.map(|v| serde_json::from_str(&v).unwrap())
.filter(|s: &Session| !s.is_expired()))
}
pub async fn delete_session(
&self,
session_id: &str
) -> Result<(), redis::RedisError> {
let mut conn = self.redis.get_async_connection().await?;
let key = format!("session:{}", session_id);
redis::cmd("DEL")
.arg(&key)
.query_async(&mut conn)
.await
}
}
Authorization 🔑
use std::collections::HashSet;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
// RBAC Implementation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Role {
name: String,
permissions: HashSet<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
id: String,
roles: HashSet<String>,
}
#[async_trait]
pub trait AuthorizationService {
async fn has_permission(
&self,
user: &User,
permission: &str
) -> bool;
async fn has_role(
&self,
user: &User,
role: &str
) -> bool;
}
pub struct RbacService {
roles: Vec<Role>,
}
#[async_trait]
impl AuthorizationService for RbacService {
async fn has_permission(
&self,
user: &User,
permission: &str
) -> bool {
for role_name in &user.roles {
if let Some(role) = self.roles
.iter()
.find(|r| &r.name == role_name)
{
if role.permissions.contains(permission) {
return true;
}
}
}
false
}
async fn has_role(
&self,
user: &User,
role: &str
) -> bool {
user.roles.contains(role)
}
}
// Authorization Middleware
use actix_web::{
dev::ServiceRequest,
error::ErrorUnauthorized,
Error,
};
use actix_web_httpauth::extractors::bearer::BearerAuth;
pub async fn auth_middleware(
req: ServiceRequest,
credentials: BearerAuth,
jwt_manager: &JwtManager,
auth_service: &impl AuthorizationService,
) -> Result<ServiceRequest, Error> {
let token = credentials.token();
// Verify JWT
let claims = jwt_manager
.verify_token(token)
.map_err(|_| ErrorUnauthorized("Invalid token"))?;
// Get user from database
let user = get_user(&claims.sub)
.await
.map_err(|_| ErrorUnauthorized("User not found"))?;
// Check required permissions
if let Some(required_permission) = req.headers()
.get("X-Required-Permission")
.and_then(|h| h.to_str().ok())
{
if !auth_service.has_permission(&user, required_permission).await {
return Err(ErrorUnauthorized("Insufficient permissions"));
}
}
Ok(req)
}
Input Validation 🛡️
use serde::Deserialize;
use validator::{Validate, ValidationError};
use regex::Regex;
// Custom Validation Rules
pub fn validate_password(password: &str) -> Result<(), ValidationError> {
let has_minimum_length = password.len() >= 8;
let has_uppercase = password
.chars()
.any(|c| c.is_uppercase());
let has_lowercase = password
.chars()
.any(|c| c.is_lowercase());
let has_digit = password
.chars()
.any(|c| c.is_digit(10));
let has_special = password
.chars()
.any(|c| !c.is_alphanumeric());
if has_minimum_length && has_uppercase && has_lowercase
&& has_digit && has_special
{
Ok(())
} else {
Err(ValidationError::new("invalid_password"))
}
}
pub fn validate_email(email: &str) -> Result<(), ValidationError> {
let email_regex = Regex::new(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
).unwrap();
if email_regex.is_match(email) {
Ok(())
} else {
Err(ValidationError::new("invalid_email"))
}
}
// Request Validation
#[derive(Debug, Deserialize, Validate)]
pub struct CreateUserRequest {
#[validate(custom = "validate_email")]
email: String,
#[validate(custom = "validate_password")]
password: String,
#[validate(length(min = 2, max = 50))]
name: String,
}
// XSS Prevention
pub fn sanitize_html(input: &str) -> String {
ammonia::clean(input)
}
// SQL Injection Prevention
use sqlx::{PgPool, query, query_as};
pub async fn safe_query(
pool: &PgPool,
user_id: &str
) -> Result<User, sqlx::Error> {
query_as!(
User,
"SELECT * FROM users WHERE id = $1",
user_id
)
.fetch_one(pool)
.await
}
// Rate Limiting
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use std::collections::HashMap;
pub struct RateLimiter {
requests: Mutex<HashMap<String, Vec<Instant>>>,
window: Duration,
max_requests: usize,
}
impl RateLimiter {
pub fn new(
window: Duration,
max_requests: usize
) -> Self {
Self {
requests: Mutex::new(HashMap::new()),
window,
max_requests,
}
}
pub async fn check_rate_limit(
&self,
key: &str
) -> bool {
let mut requests = self.requests.lock().await;
let now = Instant::now();
let timestamps = requests
.entry(key.to_string())
.or_insert_with(Vec::new);
// Remove old timestamps
timestamps.retain(|&t| now - t <= self.window);
if timestamps.len() >= self.max_requests {
false
} else {
timestamps.push(now);
true
}
}
}
Secure Communication 🔐
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
use actix_web::HttpServer;
use rustls::{ServerConfig, PrivateKey, Certificate};
use std::fs::File;
use std::io::BufReader;
// HTTPS Configuration
pub fn configure_ssl() -> SslAcceptor {
let mut builder = SslAcceptor::mozilla_intermediate(
SslMethod::tls()
).unwrap();
builder.set_private_key_file(
"key.pem",
SslFiletype::PEM
).unwrap();
builder.set_certificate_chain_file(
"cert.pem"
).unwrap();
builder.build()
}
// Rustls Configuration
pub fn configure_rustls() -> ServerConfig {
let cert_file = &mut BufReader::new(
File::open("cert.pem").unwrap()
);
let key_file = &mut BufReader::new(
File::open("key.pem").unwrap()
);
let cert_chain = rustls_pemfile::certs(cert_file)
.unwrap()
.into_iter()
.map(Certificate)
.collect();
let mut keys: Vec<PrivateKey> = rustls_pemfile::pkcs8_private_keys(key_file)
.unwrap()
.into_iter()
.map(PrivateKey)
.collect();
ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(cert_chain, keys.remove(0))
.unwrap()
}
// Secure Headers Middleware
use actix_web::middleware::{DefaultHeaders, Logger};
pub fn configure_security_headers(config: &mut web::ServiceConfig) {
config.service(
web::scope("")
.wrap(
DefaultHeaders::new()
.add((
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
))
.add((
"X-Frame-Options",
"DENY"
))
.add((
"X-Content-Type-Options",
"nosniff"
))
.add((
"X-XSS-Protection",
"1; mode=block"
))
.add((
"Content-Security-Policy",
"default-src 'self'"
))
)
.wrap(Logger::default())
);
}
// CORS Configuration
use actix_cors::Cors;
pub fn configure_cors() -> Cors {
Cors::default()
.allowed_origin("https://example.com")
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![
http::header::AUTHORIZATION,
http::header::ACCEPT,
http::header::CONTENT_TYPE,
])
.max_age(3600)
}
Error Handling 🚨
use std::error::Error;
use std::fmt;
use actix_web::{
error::ResponseError,
http::StatusCode,
HttpResponse,
};
use serde::Serialize;
// Custom Error Types
#[derive(Debug)]
pub enum AppError {
Authentication(String),
Authorization(String),
Validation(String),
Database(String),
Internal(String),
}
impl Error for AppError {}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Authentication(msg) => write!(f, "Authentication error: {}", msg),
AppError::Authorization(msg) => write!(f, "Authorization error: {}", msg),
AppError::Validation(msg) => write!(f, "Validation error: {}", msg),
AppError::Database(msg) => write!(f, "Database error: {}", msg),
AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
}
}
}
#[derive(Serialize)]
struct ErrorResponse {
code: u16,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<String>,
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
let (status, message) = match self {
AppError::Authentication(_) => (
StatusCode::UNAUTHORIZED,
"Authentication failed"
),
AppError::Authorization(_) => (
StatusCode::FORBIDDEN,
"Access denied"
),
AppError::Validation(_) => (
StatusCode::BAD_REQUEST,
"Invalid input"
),
AppError::Database(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Database error"
),
AppError::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error"
),
};
let error_response = ErrorResponse {
code: status.as_u16(),
message: message.to_string(),
details: if cfg!(debug_assertions) {
Some(self.to_string())
} else {
None
},
};
HttpResponse::build(status)
.json(error_response)
}
fn status_code(&self) -> StatusCode {
match self {
AppError::Authentication(_) => StatusCode::UNAUTHORIZED,
AppError::Authorization(_) => StatusCode::FORBIDDEN,
AppError::Validation(_) => StatusCode::BAD_REQUEST,
AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
Best Practices 📝
- Use proper authentication
- Implement proper authorization
- Validate all input
- Use secure communication
- Implement proper error handling
- Use proper password hashing
- Implement rate limiting
- Use secure headers
- Implement proper logging
- Follow security best practices
Additional Resources
Rust provides powerful tools for building secure web applications. This guide covers essential security practices and patterns for developing secure web applications with Rust.