Secure Authentication Systems in Rust
Introduction
Authentication forms the cornerstone of any secure system, yet it’s often implemented with critical vulnerabilities that expose entire infrastructures to compromise. In this comprehensive guide, we’ll build a production-ready authentication system in Rust that implements industry best practices including JWT tokens, OAuth2 integration, multi-factor authentication, and role-based access control.
Our system will demonstrate how Rust’s type safety and memory safety features help prevent common authentication vulnerabilities while delivering the performance and reliability required for enterprise-scale deployments.
Architecture Overview
Security-First Design
┌─────────────────────────────────────────────────────────────────┐│ Client Applications ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ ││ │ Web App │ │ Mobile App │ │ API Clients │ ││ └─────────────┘ └─────────────┘ └─────────────────────────┘ │├─────────────────────────────────────────────────────────────────┤│ API Gateway & Load Balancer │├─────────────────────────────────────────────────────────────────┤│ Authentication Service ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ ││ │ JWT │ │ OAuth2 │ │ MFA │ ││ │ Manager │ │ Provider │ │ Manager │ ││ └─────────────┘ └─────────────┘ └─────────────────────────┘ │├─────────────────────────────────────────────────────────────────┤│ Authorization Layer ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ ││ │ RBAC │ │ Permissions │ │ Policy Engine │ ││ │ Engine │ │ Manager │ │ │ ││ └─────────────┘ └─────────────┘ └─────────────────────────┘ │├─────────────────────────────────────────────────────────────────┤│ Data Layer ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ ││ │ User │ │ Session │ │ Audit Log │ ││ │ Database │ │ Store │ │ │ ││ └─────────────┘ └─────────────┘ └─────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘
Core Components
// Core authentication system architecturepub struct AuthenticationSystem { jwt_manager: Arc<JwtManager>, oauth2_provider: Arc<OAuth2Provider>, mfa_manager: Arc<MfaManager>, rbac_engine: Arc<RbacEngine>, user_service: Arc<UserService>, session_store: Arc<SessionStore>, audit_logger: Arc<AuditLogger>, rate_limiter: Arc<RateLimiter>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct AuthenticationRequest { pub username: String, pub password: SecureString, pub mfa_token: Option<String>, pub client_info: ClientInfo, pub requested_scopes: Vec<String>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct AuthenticationResponse { pub access_token: String, pub refresh_token: String, pub token_type: String, pub expires_in: u64, pub scope: String, pub user_info: UserInfo,}
Project Setup
Dependencies
[package]name = "secure-auth-system"version = "1.0.0"edition = "2021"
[dependencies]# Web frameworkaxum = { version = "0.7", features = ["macros", "multipart"] }tower = { version = "0.4", features = ["full"] }tower-http = { version = "0.5", features = ["auth", "cors", "trace", "rate-limit"] }hyper = { version = "1.0", features = ["full"] }
# Async runtimetokio = { version = "1.35", features = ["full"] }tokio-util = { version = "0.7", features = ["codec"] }
# Serializationserde = { version = "1.0", features = ["derive"] }serde_json = "1.0"serde_urlencoded = "0.7"
# HTTP clientreqwest = { version = "0.11", features = ["json", "rustls-tls"] }
# Cryptographyring = "0.17"rustls = "0.22"rustls-webpki = "0.102"password-hash = "0.5"argon2 = "0.5"hmac = "0.12"sha2 = "0.10"aes-gcm = "0.10"rand = "0.8"rand_core = { version = "0.6", features = ["std"] }
# JWTjsonwebtoken = "9.2"base64 = "0.21"
# Databasesqlx = { version = "0.7", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid", "json"] }redis = { version = "0.24", features = ["tokio-comp", "connection-manager"] }
# Utilitiesuuid = { version = "1.6", features = ["v4", "serde"] }chrono = { version = "0.4", features = ["serde"] }anyhow = "1.0"thiserror = "1.0"tracing = "0.1"tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# OAuth2oauth2 = "4.4"url = "2.5"
# TOTP for MFAtotp-rs = { version = "5.4", features = ["qr"] }qrcode = "0.14"image = "0.24"
# Rate limitinggovernor = "0.6"nonzero_ext = "0.3"
# Securitysecrecy = { version = "0.8", features = ["serde"] }zeroize = "1.7"
# Configurationconfig = "0.13"clap = { version = "4.4", features = ["derive"] }
# Metricsprometheus = { version = "0.13", features = ["process"] }metrics = "0.22"metrics-prometheus = "0.6"
[dev-dependencies]tempfile = "3.8"mockall = "0.12"wiremock = "0.5"proptest = "1.4"criterion = "0.5"
Database Schema
-- migrations/001_initial_schema.sql
-- Users table with security enhancementsCREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, salt VARCHAR(255) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true, is_verified BOOLEAN NOT NULL DEFAULT false, failed_login_attempts INTEGER NOT NULL DEFAULT 0, locked_until TIMESTAMP WITH TIME ZONE, last_login TIMESTAMP WITH TIME ZONE, password_changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- Security constraints CONSTRAINT users_username_length CHECK (char_length(username) >= 3), CONSTRAINT users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'));
-- User profiles for additional informationCREATE TABLE user_profiles ( user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, first_name VARCHAR(255), last_name VARCHAR(255), phone_number VARCHAR(20), timezone VARCHAR(50) DEFAULT 'UTC', locale VARCHAR(10) DEFAULT 'en-US', avatar_url TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
-- Roles and permissions (RBAC)CREATE TABLE roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) UNIQUE NOT NULL, description TEXT, is_system_role BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
CREATE TABLE permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) UNIQUE NOT NULL, resource VARCHAR(100) NOT NULL, action VARCHAR(50) NOT NULL, description TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(resource, action));
CREATE TABLE role_permissions ( role_id UUID REFERENCES roles(id) ON DELETE CASCADE, permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE, granted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), granted_by UUID REFERENCES users(id),
PRIMARY KEY (role_id, permission_id));
CREATE TABLE user_roles ( user_id UUID REFERENCES users(id) ON DELETE CASCADE, role_id UUID REFERENCES roles(id) ON DELETE CASCADE, assigned_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), assigned_by UUID REFERENCES users(id), expires_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (user_id, role_id));
-- MFA configurationCREATE TABLE user_mfa ( user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, is_enabled BOOLEAN NOT NULL DEFAULT false, secret_key VARCHAR(255), -- Encrypted TOTP secret backup_codes TEXT[], -- Encrypted backup codes recovery_codes_used INTEGER NOT NULL DEFAULT 0, last_used_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
-- OAuth2 clients and tokensCREATE TABLE oauth2_clients ( client_id VARCHAR(255) PRIMARY KEY, client_secret_hash VARCHAR(255) NOT NULL, client_name VARCHAR(255) NOT NULL, redirect_uris TEXT[] NOT NULL, allowed_scopes TEXT[] NOT NULL, is_confidential BOOLEAN NOT NULL DEFAULT true, token_lifetime_seconds INTEGER NOT NULL DEFAULT 3600, refresh_token_lifetime_seconds INTEGER NOT NULL DEFAULT 86400, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
CREATE TABLE oauth2_authorization_codes ( code VARCHAR(255) PRIMARY KEY, client_id VARCHAR(255) NOT NULL REFERENCES oauth2_clients(client_id), user_id UUID NOT NULL REFERENCES users(id), redirect_uri TEXT NOT NULL, scope TEXT NOT NULL, code_challenge VARCHAR(255), code_challenge_method VARCHAR(10), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
CREATE TABLE oauth2_access_tokens ( token_hash VARCHAR(255) PRIMARY KEY, client_id VARCHAR(255) NOT NULL REFERENCES oauth2_clients(client_id), user_id UUID NOT NULL REFERENCES users(id), scope TEXT NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
CREATE TABLE oauth2_refresh_tokens ( token_hash VARCHAR(255) PRIMARY KEY, access_token_hash VARCHAR(255) NOT NULL, client_id VARCHAR(255) NOT NULL REFERENCES oauth2_clients(client_id), user_id UUID NOT NULL REFERENCES users(id), scope TEXT NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
-- Session managementCREATE TABLE user_sessions ( session_id VARCHAR(255) PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id), client_info JSONB NOT NULL, ip_address INET, user_agent TEXT, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, last_activity TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), is_revoked BOOLEAN NOT NULL DEFAULT false, revoked_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
-- Audit loggingCREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id), action VARCHAR(100) NOT NULL, resource VARCHAR(100), resource_id VARCHAR(255), ip_address INET, user_agent TEXT, request_id VARCHAR(255), details JSONB, severity VARCHAR(20) NOT NULL DEFAULT 'INFO', timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW());
-- Indexes for performanceCREATE INDEX idx_users_email ON users(email);CREATE INDEX idx_users_username ON users(username);CREATE INDEX idx_users_last_login ON users(last_login);CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id);CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at);CREATE INDEX idx_oauth2_access_tokens_expires_at ON oauth2_access_tokens(expires_at);CREATE INDEX idx_oauth2_refresh_tokens_expires_at ON oauth2_refresh_tokens(expires_at);CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp);
-- Initial system roles and permissionsINSERT INTO roles (name, description, is_system_role) VALUES ('super_admin', 'System administrator with full access', true), ('admin', 'Administrator with most permissions', true), ('user', 'Standard user with basic permissions', true), ('readonly', 'Read-only access to resources', true);
INSERT INTO permissions (name, resource, action, description) VALUES ('users.create', 'users', 'create', 'Create new users'), ('users.read', 'users', 'read', 'View user information'), ('users.update', 'users', 'update', 'Update user information'), ('users.delete', 'users', 'delete', 'Delete users'), ('roles.manage', 'roles', 'manage', 'Manage roles and permissions'), ('system.admin', 'system', 'admin', 'System administration access'), ('audit.read', 'audit', 'read', 'View audit logs');
JWT Token Management
Secure JWT Implementation
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};use serde::{Deserialize, Serialize};use std::collections::HashSet;use chrono::{DateTime, Duration, Utc};use uuid::Uuid;use anyhow::Result;
#[derive(Debug, Clone)]pub struct JwtManager { encoding_key: EncodingKey, decoding_key: DecodingKey, algorithm: Algorithm, access_token_lifetime: Duration, refresh_token_lifetime: Duration, issuer: String, audience: HashSet<String>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct Claims { // Standard JWT claims pub sub: String, // Subject (user ID) pub iss: String, // Issuer pub aud: Vec<String>, // Audience pub exp: i64, // Expiration time pub iat: i64, // Issued at pub nbf: i64, // Not before pub jti: String, // JWT ID
// Custom claims pub username: String, pub email: String, pub roles: Vec<String>, pub permissions: Vec<String>, pub scope: String, pub token_type: TokenType, pub session_id: String, pub client_id: Option<String>,}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]#[serde(rename_all = "lowercase")]pub enum TokenType { Access, Refresh, IdToken,}
#[derive(Debug, Clone)]pub struct TokenPair { pub access_token: String, pub refresh_token: String, pub expires_in: u64, pub token_type: String,}
impl JwtManager { pub fn new( secret: &[u8], issuer: String, audience: HashSet<String>, access_token_lifetime: Duration, refresh_token_lifetime: Duration, ) -> Result<Self> { // Use RS256 for production (requires RSA key pair) // For this example, we'll use HS256 with a secure secret let encoding_key = EncodingKey::from_secret(secret); let decoding_key = DecodingKey::from_secret(secret);
Ok(Self { encoding_key, decoding_key, algorithm: Algorithm::HS256, access_token_lifetime, refresh_token_lifetime, issuer, audience, }) }
pub fn create_token_pair( &self, user_id: Uuid, username: String, email: String, roles: Vec<String>, permissions: Vec<String>, scope: String, session_id: String, client_id: Option<String>, ) -> Result<TokenPair> { let now = Utc::now();
// Create access token let access_claims = Claims { sub: user_id.to_string(), iss: self.issuer.clone(), aud: self.audience.iter().cloned().collect(), exp: (now + self.access_token_lifetime).timestamp(), iat: now.timestamp(), nbf: now.timestamp(), jti: Uuid::new_v4().to_string(), username: username.clone(), email: email.clone(), roles: roles.clone(), permissions, scope: scope.clone(), token_type: TokenType::Access, session_id: session_id.clone(), client_id: client_id.clone(), };
let access_token = encode( &Header::new(self.algorithm), &access_claims, &self.encoding_key, )?;
// Create refresh token let refresh_claims = Claims { sub: user_id.to_string(), iss: self.issuer.clone(), aud: self.audience.iter().cloned().collect(), exp: (now + self.refresh_token_lifetime).timestamp(), iat: now.timestamp(), nbf: now.timestamp(), jti: Uuid::new_v4().to_string(), username, email, roles, permissions: vec![], // Refresh tokens don't need permissions scope, token_type: TokenType::Refresh, session_id, client_id, };
let refresh_token = encode( &Header::new(self.algorithm), &refresh_claims, &self.encoding_key, )?;
Ok(TokenPair { access_token, refresh_token, expires_in: self.access_token_lifetime.num_seconds() as u64, token_type: "Bearer".to_string(), }) }
pub fn verify_token(&self, token: &str) -> Result<Claims> { let mut validation = Validation::new(self.algorithm); validation.set_issuer(&[&self.issuer]); validation.set_audience(&self.audience); validation.validate_exp = true; validation.validate_nbf = true;
let token_data = decode::<Claims>(token, &self.decoding_key, &validation)?; Ok(token_data.claims) }
pub fn refresh_access_token( &self, refresh_token: &str, user_service: &UserService, ) -> Result<TokenPair> { // Verify refresh token let claims = self.verify_token(refresh_token)?;
// Ensure it's a refresh token if claims.token_type != TokenType::Refresh { return Err(anyhow::anyhow!("Invalid token type for refresh")); }
// Get updated user information let user_id = Uuid::parse_str(&claims.sub)?; let user = user_service.get_user_by_id(user_id)? .ok_or_else(|| anyhow::anyhow!("User not found"))?;
// Check if user is still active if !user.is_active { return Err(anyhow::anyhow!("User account is inactive")); }
// Get current roles and permissions let roles = user_service.get_user_roles(user_id)?; let permissions = user_service.get_user_permissions(user_id)?;
// Create new token pair self.create_token_pair( user_id, user.username, user.email, roles, permissions, claims.scope, claims.session_id, claims.client_id, ) }
pub fn extract_bearer_token(authorization_header: &str) -> Option<&str> { authorization_header .strip_prefix("Bearer ") .map(|token| token.trim()) }
pub fn validate_token_permissions(&self, token: &str, required_permission: &str) -> Result<bool> { let claims = self.verify_token(token)?; Ok(claims.permissions.contains(&required_permission.to_string())) }
pub fn validate_token_roles(&self, token: &str, required_roles: &[String]) -> Result<bool> { let claims = self.verify_token(token)?; Ok(required_roles.iter().any(|role| claims.roles.contains(role))) }
pub fn get_token_info(&self, token: &str) -> Result<TokenInfo> { let claims = self.verify_token(token)?;
Ok(TokenInfo { user_id: Uuid::parse_str(&claims.sub)?, username: claims.username, email: claims.email, roles: claims.roles, permissions: claims.permissions, expires_at: DateTime::from_timestamp(claims.exp, 0) .ok_or_else(|| anyhow::anyhow!("Invalid expiration timestamp"))?, session_id: claims.session_id, client_id: claims.client_id, }) }}
#[derive(Debug, Clone)]pub struct TokenInfo { pub user_id: Uuid, pub username: String, pub email: String, pub roles: Vec<String>, pub permissions: Vec<String>, pub expires_at: DateTime<Utc>, pub session_id: String, pub client_id: Option<String>,}
// Secure token storage for refresh tokens#[derive(Debug, Clone)]pub struct TokenStore { redis: redis::Client,}
impl TokenStore { pub fn new(redis_url: &str) -> Result<Self> { let redis = redis::Client::open(redis_url)?; Ok(Self { redis }) }
pub async fn store_refresh_token( &self, user_id: Uuid, token_hash: &str, expires_at: DateTime<Utc>, ) -> Result<()> { let mut conn = self.redis.get_multiplexed_async_connection().await?; let key = format!("refresh_token:{}", user_id); let ttl = (expires_at - Utc::now()).num_seconds();
redis::cmd("SETEX") .arg(&key) .arg(ttl) .arg(token_hash) .exec_async(&mut conn) .await?;
Ok(()) }
pub async fn validate_refresh_token( &self, user_id: Uuid, token_hash: &str, ) -> Result<bool> { let mut conn = self.redis.get_multiplexed_async_connection().await?; let key = format!("refresh_token:{}", user_id);
let stored_hash: Option<String> = redis::cmd("GET") .arg(&key) .query_async(&mut conn) .await?;
Ok(stored_hash.as_deref() == Some(token_hash)) }
pub async fn revoke_refresh_token(&self, user_id: Uuid) -> Result<()> { let mut conn = self.redis.get_multiplexed_async_connection().await?; let key = format!("refresh_token:{}", user_id);
redis::cmd("DEL") .arg(&key) .exec_async(&mut conn) .await?;
Ok(()) }
pub async fn revoke_all_user_tokens(&self, user_id: Uuid) -> Result<()> { let mut conn = self.redis.get_multiplexed_async_connection().await?; let pattern = format!("*:{}:*", user_id);
let keys: Vec<String> = redis::cmd("KEYS") .arg(&pattern) .query_async(&mut conn) .await?;
if !keys.is_empty() { redis::cmd("DEL") .arg(&keys) .exec_async(&mut conn) .await?; }
Ok(()) }}
// JWT Middleware for Axum#[derive(Clone)]pub struct JwtAuthLayer { jwt_manager: Arc<JwtManager>,}
impl JwtAuthLayer { pub fn new(jwt_manager: Arc<JwtManager>) -> Self { Self { jwt_manager } }}
use axum::{ extract::Request, http::{HeaderMap, StatusCode}, middleware::Next, response::Response,};
pub async fn jwt_auth_middleware( headers: HeaderMap, mut request: Request, next: Next,) -> Result<Response, StatusCode> { let auth_header = headers .get("authorization") .and_then(|h| h.to_str().ok()) .ok_or(StatusCode::UNAUTHORIZED)?;
let token = JwtManager::extract_bearer_token(auth_header) .ok_or(StatusCode::UNAUTHORIZED)?;
// Get JWT manager from app state let jwt_manager = request .extensions() .get::<Arc<JwtManager>>() .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let claims = jwt_manager .verify_token(token) .map_err(|_| StatusCode::UNAUTHORIZED)?;
// Add claims to request extensions request.extensions_mut().insert(claims);
Ok(next.run(request).await)}
// Permission-based authorization middlewarepub fn require_permission(permission: &'static str) -> impl Fn(Request, Next) -> impl std::future::Future<Output = Result<Response, StatusCode>> + Clone { move |request: Request, next: Next| async move { let claims = request .extensions() .get::<Claims>() .ok_or(StatusCode::UNAUTHORIZED)?;
if !claims.permissions.contains(&permission.to_string()) { return Err(StatusCode::FORBIDDEN); }
Ok(next.run(request).await) }}
// Role-based authorization middlewarepub fn require_role(roles: &'static [&'static str]) -> impl Fn(Request, Next) -> impl std::future::Future<Output = Result<Response, StatusCode>> + Clone { move |request: Request, next: Next| async move { let claims = request .extensions() .get::<Claims>() .ok_or(StatusCode::UNAUTHORIZED)?;
let has_role = roles.iter().any(|role| claims.roles.contains(&role.to_string())); if !has_role { return Err(StatusCode::FORBIDDEN); }
Ok(next.run(request).await) }}
Multi-Factor Authentication (MFA)
TOTP Implementation
use totp_rs::{Algorithm, Secret, TOTP};use qrcode::{QrCode, render::svg};use ring::rand::{SecureRandom, SystemRandom};use aes_gcm::{Aes256Gcm, Key, Nonce, aead::{Aead, NewAead}};use base64::{Engine as _, engine::general_purpose};use serde::{Deserialize, Serialize};use uuid::Uuid;use chrono::{DateTime, Utc};use anyhow::Result;
#[derive(Debug, Clone)]pub struct MfaManager { encryption_key: [u8; 32], issuer: String, backup_codes_count: usize,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct MfaSetup { pub secret_key: String, pub qr_code_svg: String, pub backup_codes: Vec<String>, pub setup_uri: String,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct MfaConfig { pub user_id: Uuid, pub is_enabled: bool, pub secret_key_encrypted: String, pub backup_codes_encrypted: Vec<String>, pub recovery_codes_used: i32, pub last_used_at: Option<DateTime<Utc>>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct MfaVerificationRequest { pub user_id: Uuid, pub token: String, pub backup_code: Option<String>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct MfaVerificationResponse { pub is_valid: bool, pub backup_code_used: bool, pub remaining_backup_codes: usize,}
impl MfaManager { pub fn new(encryption_key: [u8; 32], issuer: String) -> Self { Self { encryption_key, issuer, backup_codes_count: 10, } }
pub fn generate_mfa_setup(&self, user_id: Uuid, username: &str, email: &str) -> Result<MfaSetup> { // Generate a random secret key let secret = Secret::generate_secret(); let secret_str = secret.to_encoded().to_string();
// Create TOTP instance let totp = TOTP::new( Algorithm::SHA1, 6, // 6-digit codes 1, // 30-second time step 30, secret.to_bytes().unwrap(), Some(self.issuer.clone()), username.to_string(), )?;
// Generate QR code let setup_uri = totp.get_url(); let qr_code = QrCode::new(&setup_uri)?; let qr_code_svg = qr_code.render::<svg::Color>() .min_dimensions(200, 200) .dark_color(svg::Color("#000000")) .light_color(svg::Color("#ffffff")) .build();
// Generate backup codes let backup_codes = self.generate_backup_codes()?;
Ok(MfaSetup { secret_key: secret_str, qr_code_svg, backup_codes, setup_uri, }) }
pub fn enable_mfa(&self, user_id: Uuid, secret_key: &str, backup_codes: &[String]) -> Result<MfaConfig> { // Encrypt secret key and backup codes let secret_encrypted = self.encrypt_data(secret_key.as_bytes())?; let backup_codes_encrypted: Result<Vec<String>> = backup_codes .iter() .map(|code| self.encrypt_data(code.as_bytes())) .collect();
Ok(MfaConfig { user_id, is_enabled: true, secret_key_encrypted: secret_encrypted, backup_codes_encrypted: backup_codes_encrypted?, recovery_codes_used: 0, last_used_at: None, }) }
pub fn verify_totp_token(&self, config: &MfaConfig, token: &str) -> Result<bool> { if !config.is_enabled { return Ok(false); }
// Decrypt secret key let secret_key = self.decrypt_data(&config.secret_key_encrypted)?; let secret = Secret::Encoded(String::from_utf8(secret_key)?);
// Create TOTP instance let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap(), Some(self.issuer.clone()), "user".to_string(), // Username not needed for verification )?;
// Verify token with time tolerance (allows for clock skew) let current_time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs();
// Check current time step and adjacent time steps for time_step_offset in [-1, 0, 1] { let check_time = (current_time as i64 + (time_step_offset * 30)) as u64; if totp.generate(check_time) == token { return Ok(true); } }
Ok(false) }
pub fn verify_backup_code(&self, config: &mut MfaConfig, backup_code: &str) -> Result<bool> { if !config.is_enabled { return Ok(false); }
// Check if backup code exists and hasn't been used for (index, encrypted_code) in config.backup_codes_encrypted.iter().enumerate() { let decrypted_code = self.decrypt_data(encrypted_code)?; let code_str = String::from_utf8(decrypted_code)?;
if code_str == backup_code { // Mark code as used by removing it config.backup_codes_encrypted.remove(index); config.recovery_codes_used += 1; return Ok(true); } }
Ok(false) }
pub async fn verify_mfa( &self, mut config: MfaConfig, request: &MfaVerificationRequest, ) -> Result<MfaVerificationResponse> { let mut backup_code_used = false; let mut is_valid = false;
// First try TOTP token if provided if !request.token.is_empty() { is_valid = self.verify_totp_token(&config, &request.token)?; }
// If TOTP failed and backup code provided, try backup code if !is_valid && request.backup_code.is_some() { let backup_code = request.backup_code.as_ref().unwrap(); is_valid = self.verify_backup_code(&mut config, backup_code)?; backup_code_used = is_valid; }
// Update last used time if verification succeeded if is_valid { // This would typically be saved to database // config.last_used_at = Some(Utc::now()); }
Ok(MfaVerificationResponse { is_valid, backup_code_used, remaining_backup_codes: config.backup_codes_encrypted.len(), }) }
pub fn generate_backup_codes(&self) -> Result<Vec<String>> { let rng = SystemRandom::new(); let mut codes = Vec::with_capacity(self.backup_codes_count);
for _ in 0..self.backup_codes_count { let mut code_bytes = [0u8; 5]; rng.fill(&mut code_bytes)?;
// Convert to readable format (XXXXX-XXXXX) let code = format!( "{:05}-{:05}", u32::from_be_bytes([0, code_bytes[0], code_bytes[1], code_bytes[2]]) % 100000, u32::from_be_bytes([0, code_bytes[2], code_bytes[3], code_bytes[4]]) % 100000 ); codes.push(code); }
Ok(codes) }
fn encrypt_data(&self, data: &[u8]) -> Result<String> { let cipher = Aes256Gcm::new(Key::from_slice(&self.encryption_key));
// Generate random nonce let rng = SystemRandom::new(); let mut nonce_bytes = [0u8; 12]; rng.fill(&mut nonce_bytes)?; let nonce = Nonce::from_slice(&nonce_bytes);
// Encrypt data let ciphertext = cipher.encrypt(nonce, data) .map_err(|e| anyhow::anyhow!("Encryption failed: {}", e))?;
// Combine nonce and ciphertext let mut result = nonce_bytes.to_vec(); result.extend_from_slice(&ciphertext);
Ok(general_purpose::STANDARD.encode(&result)) }
fn decrypt_data(&self, encrypted_data: &str) -> Result<Vec<u8>> { let cipher = Aes256Gcm::new(Key::from_slice(&self.encryption_key));
// Decode base64 let data = general_purpose::STANDARD.decode(encrypted_data)?;
if data.len() < 12 { return Err(anyhow::anyhow!("Invalid encrypted data length")); }
// Extract nonce and ciphertext let (nonce_bytes, ciphertext) = data.split_at(12); let nonce = Nonce::from_slice(nonce_bytes);
// Decrypt data let plaintext = cipher.decrypt(nonce, ciphertext) .map_err(|e| anyhow::anyhow!("Decryption failed: {}", e))?;
Ok(plaintext) }
pub fn disable_mfa(&self, config: &mut MfaConfig) -> Result<()> { config.is_enabled = false; config.secret_key_encrypted.clear(); config.backup_codes_encrypted.clear(); config.recovery_codes_used = 0; config.last_used_at = None; Ok(()) }
pub fn regenerate_backup_codes(&self, config: &mut MfaConfig) -> Result<Vec<String>> { let new_codes = self.generate_backup_codes()?;
// Encrypt new codes let encrypted_codes: Result<Vec<String>> = new_codes .iter() .map(|code| self.encrypt_data(code.as_bytes())) .collect();
config.backup_codes_encrypted = encrypted_codes?; config.recovery_codes_used = 0;
Ok(new_codes) }
pub fn get_mfa_status(&self, config: &MfaConfig) -> MfaStatus { MfaStatus { is_enabled: config.is_enabled, backup_codes_remaining: config.backup_codes_encrypted.len(), last_used_at: config.last_used_at, setup_required: !config.is_enabled && config.secret_key_encrypted.is_empty(), } }}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct MfaStatus { pub is_enabled: bool, pub backup_codes_remaining: usize, pub last_used_at: Option<DateTime<Utc>>, pub setup_required: bool,}
// Rate limiting for MFA attempts#[derive(Debug, Clone)]pub struct MfaRateLimiter { redis: redis::Client, max_attempts: u32, window_seconds: u64, lockout_duration_seconds: u64,}
impl MfaRateLimiter { pub fn new(redis_url: &str) -> Result<Self> { Ok(Self { redis: redis::Client::open(redis_url)?, max_attempts: 5, window_seconds: 300, // 5 minutes lockout_duration_seconds: 900, // 15 minutes }) }
pub async fn check_rate_limit(&self, user_id: Uuid) -> Result<bool> { let mut conn = self.redis.get_multiplexed_async_connection().await?; let key = format!("mfa_attempts:{}", user_id);
// Check if user is locked out let lockout_key = format!("mfa_lockout:{}", user_id); let is_locked: Option<String> = redis::cmd("GET") .arg(&lockout_key) .query_async(&mut conn) .await?;
if is_locked.is_some() { return Ok(false); // Rate limited }
// Check attempt count let attempts: Option<u32> = redis::cmd("GET") .arg(&key) .query_async(&mut conn) .await?;
let current_attempts = attempts.unwrap_or(0);
if current_attempts >= self.max_attempts { // Lock out user redis::cmd("SETEX") .arg(&lockout_key) .arg(self.lockout_duration_seconds) .arg("locked") .exec_async(&mut conn) .await?;
// Clear attempts counter redis::cmd("DEL") .arg(&key) .exec_async(&mut conn) .await?;
return Ok(false); }
Ok(true) }
pub async fn record_attempt(&self, user_id: Uuid, success: bool) -> Result<()> { let mut conn = self.redis.get_multiplexed_async_connection().await?; let key = format!("mfa_attempts:{}", user_id);
if success { // Clear attempts on success redis::cmd("DEL") .arg(&key) .exec_async(&mut conn) .await?; } else { // Increment attempts redis::cmd("INCR") .arg(&key) .exec_async(&mut conn) .await?;
// Set expiration on first attempt redis::cmd("EXPIRE") .arg(&key) .arg(self.window_seconds) .exec_async(&mut conn) .await?; }
Ok(()) }}
OAuth2 Implementation
OAuth2 Authorization Server
use oauth2::{ AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, TokenUrl,};use serde::{Deserialize, Serialize};use uuid::Uuid;use chrono::{DateTime, Duration, Utc};use url::Url;use anyhow::Result;use std::collections::HashMap;
#[derive(Debug, Clone)]pub struct OAuth2Provider { clients: HashMap<String, OAuth2Client>, auth_codes: Arc<Mutex<HashMap<String, AuthorizationCodeGrant>>>, access_tokens: Arc<Mutex<HashMap<String, AccessTokenGrant>>>, refresh_tokens: Arc<Mutex<HashMap<String, RefreshTokenGrant>>>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct OAuth2Client { pub client_id: String, pub client_secret_hash: String, pub client_name: String, pub redirect_uris: Vec<String>, pub allowed_scopes: Vec<String>, pub is_confidential: bool, pub token_lifetime_seconds: u64, pub refresh_token_lifetime_seconds: u64,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct AuthorizationRequest { pub response_type: String, pub client_id: String, pub redirect_uri: String, pub scope: Option<String>, pub state: Option<String>, pub code_challenge: Option<String>, pub code_challenge_method: Option<String>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct AuthorizationResponse { pub code: String, pub state: Option<String>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct TokenRequest { pub grant_type: String, pub code: Option<String>, pub redirect_uri: Option<String>, pub client_id: String, pub client_secret: Option<String>, pub code_verifier: Option<String>, pub refresh_token: Option<String>, pub scope: Option<String>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct TokenResponse { pub access_token: String, pub token_type: String, pub expires_in: u64, pub refresh_token: Option<String>, pub scope: String, pub id_token: Option<String>,}
#[derive(Debug, Clone)]struct AuthorizationCodeGrant { client_id: String, user_id: Uuid, redirect_uri: String, scope: String, code_challenge: Option<String>, code_challenge_method: Option<String>, expires_at: DateTime<Utc>,}
#[derive(Debug, Clone)]struct AccessTokenGrant { client_id: String, user_id: Uuid, scope: String, expires_at: DateTime<Utc>,}
#[derive(Debug, Clone)]struct RefreshTokenGrant { access_token_hash: String, client_id: String, user_id: Uuid, scope: String, expires_at: DateTime<Utc>,}
impl OAuth2Provider { pub fn new() -> Self { Self { clients: HashMap::new(), auth_codes: Arc::new(Mutex::new(HashMap::new())), access_tokens: Arc::new(Mutex::new(HashMap::new())), refresh_tokens: Arc::new(Mutex::new(HashMap::new())), } }
pub fn register_client(&mut self, client: OAuth2Client) { self.clients.insert(client.client_id.clone(), client); }
pub fn authorize( &self, request: &AuthorizationRequest, user_id: Uuid, ) -> Result<AuthorizationResponse> { // Validate client let client = self.clients.get(&request.client_id) .ok_or_else(|| anyhow::anyhow!("Invalid client_id"))?;
// Validate redirect URI if !client.redirect_uris.contains(&request.redirect_uri) { return Err(anyhow::anyhow!("Invalid redirect_uri")); }
// Validate response type if request.response_type != "code" { return Err(anyhow::anyhow!("Unsupported response_type")); }
// Validate and parse scopes let requested_scopes = request.scope .as_ref() .map(|s| s.split_whitespace().collect::<Vec<_>>()) .unwrap_or_default();
// Check if all requested scopes are allowed for scope in &requested_scopes { if !client.allowed_scopes.contains(&scope.to_string()) { return Err(anyhow::anyhow!("Invalid scope: {}", scope)); } }
// Generate authorization code let auth_code = self.generate_authorization_code(); let expires_at = Utc::now() + Duration::minutes(10); // 10-minute expiry
// Store authorization grant let grant = AuthorizationCodeGrant { client_id: request.client_id.clone(), user_id, redirect_uri: request.redirect_uri.clone(), scope: requested_scopes.join(" "), code_challenge: request.code_challenge.clone(), code_challenge_method: request.code_challenge_method.clone(), expires_at, };
self.auth_codes.lock().unwrap().insert(auth_code.clone(), grant);
Ok(AuthorizationResponse { code: auth_code, state: request.state.clone(), }) }
pub fn exchange_code_for_token( &self, request: &TokenRequest, jwt_manager: &JwtManager, user_service: &UserService, ) -> Result<TokenResponse> { match request.grant_type.as_str() { "authorization_code" => self.handle_authorization_code_grant(request, jwt_manager, user_service), "refresh_token" => self.handle_refresh_token_grant(request, jwt_manager, user_service), _ => Err(anyhow::anyhow!("Unsupported grant_type")), } }
fn handle_authorization_code_grant( &self, request: &TokenRequest, jwt_manager: &JwtManager, user_service: &UserService, ) -> Result<TokenResponse> { let code = request.code.as_ref() .ok_or_else(|| anyhow::anyhow!("Missing authorization code"))?;
// Get and remove authorization code grant let grant = { let mut codes = self.auth_codes.lock().unwrap(); codes.remove(code) .ok_or_else(|| anyhow::anyhow!("Invalid or expired authorization code"))? };
// Validate client let client = self.clients.get(&request.client_id) .ok_or_else(|| anyhow::anyhow!("Invalid client_id"))?;
if grant.client_id != request.client_id { return Err(anyhow::anyhow!("Client ID mismatch")); }
// Check expiration if Utc::now() > grant.expires_at { return Err(anyhow::anyhow!("Authorization code expired")); }
// Validate redirect URI if let Some(redirect_uri) = &request.redirect_uri { if *redirect_uri != grant.redirect_uri { return Err(anyhow::anyhow!("Redirect URI mismatch")); } }
// Validate PKCE if present if let Some(code_challenge) = &grant.code_challenge { let code_verifier = request.code_verifier.as_ref() .ok_or_else(|| anyhow::anyhow!("Missing code_verifier for PKCE"))?;
if !self.verify_pkce_challenge(code_challenge, code_verifier, &grant.code_challenge_method)? { return Err(anyhow::anyhow!("Invalid PKCE code_verifier")); } }
// Authenticate confidential clients if client.is_confidential { let client_secret = request.client_secret.as_ref() .ok_or_else(|| anyhow::anyhow!("Missing client_secret"))?;
if !self.verify_client_secret(client, client_secret)? { return Err(anyhow::anyhow!("Invalid client_secret")); } }
// Get user information let user = user_service.get_user_by_id(grant.user_id)? .ok_or_else(|| anyhow::anyhow!("User not found"))?;
if !user.is_active { return Err(anyhow::anyhow!("User account is inactive")); }
// Get user roles and permissions let roles = user_service.get_user_roles(grant.user_id)?; let permissions = user_service.get_user_permissions(grant.user_id)?;
// Generate session ID let session_id = Uuid::new_v4().to_string();
// Create JWT tokens let token_pair = jwt_manager.create_token_pair( grant.user_id, user.username.clone(), user.email.clone(), roles, permissions, grant.scope.clone(), session_id, Some(client.client_id.clone()), )?;
// Store access token grant let access_token_hash = self.hash_token(&token_pair.access_token)?; let access_grant = AccessTokenGrant { client_id: client.client_id.clone(), user_id: grant.user_id, scope: grant.scope.clone(), expires_at: Utc::now() + Duration::seconds(client.token_lifetime_seconds as i64), };
self.access_tokens.lock().unwrap().insert(access_token_hash.clone(), access_grant);
// Store refresh token grant if refresh token is provided let refresh_token = if !token_pair.refresh_token.is_empty() { let refresh_token_hash = self.hash_token(&token_pair.refresh_token)?; let refresh_grant = RefreshTokenGrant { access_token_hash: access_token_hash.clone(), client_id: client.client_id.clone(), user_id: grant.user_id, scope: grant.scope.clone(), expires_at: Utc::now() + Duration::seconds(client.refresh_token_lifetime_seconds as i64), };
self.refresh_tokens.lock().unwrap().insert(refresh_token_hash, refresh_grant); Some(token_pair.refresh_token) } else { None };
Ok(TokenResponse { access_token: token_pair.access_token, token_type: token_pair.token_type, expires_in: token_pair.expires_in, refresh_token, scope: grant.scope, id_token: None, // Would implement OpenID Connect ID token here }) }
fn handle_refresh_token_grant( &self, request: &TokenRequest, jwt_manager: &JwtManager, user_service: &UserService, ) -> Result<TokenResponse> { let refresh_token = request.refresh_token.as_ref() .ok_or_else(|| anyhow::anyhow!("Missing refresh_token"))?;
// Validate client let client = self.clients.get(&request.client_id) .ok_or_else(|| anyhow::anyhow!("Invalid client_id"))?;
// Authenticate confidential clients if client.is_confidential { let client_secret = request.client_secret.as_ref() .ok_or_else(|| anyhow::anyhow!("Missing client_secret"))?;
if !self.verify_client_secret(client, client_secret)? { return Err(anyhow::anyhow!("Invalid client_secret")); } }
// Get and validate refresh token grant let refresh_token_hash = self.hash_token(refresh_token)?; let grant = { let refresh_tokens = self.refresh_tokens.lock().unwrap(); refresh_tokens.get(&refresh_token_hash) .ok_or_else(|| anyhow::anyhow!("Invalid refresh_token"))? .clone() };
if grant.client_id != request.client_id { return Err(anyhow::anyhow!("Client ID mismatch")); }
// Check expiration if Utc::now() > grant.expires_at { return Err(anyhow::anyhow!("Refresh token expired")); }
// Revoke old tokens self.revoke_access_token(&grant.access_token_hash)?; self.revoke_refresh_token(&refresh_token_hash)?;
// Get user information let user = user_service.get_user_by_id(grant.user_id)? .ok_or_else(|| anyhow::anyhow!("User not found"))?;
if !user.is_active { return Err(anyhow::anyhow!("User account is inactive")); }
// Get current roles and permissions let roles = user_service.get_user_roles(grant.user_id)?; let permissions = user_service.get_user_permissions(grant.user_id)?;
// Handle scope parameter let scope = if let Some(requested_scope) = &request.scope { // Validate that requested scope is a subset of original scope let original_scopes: Vec<&str> = grant.scope.split_whitespace().collect(); let requested_scopes: Vec<&str> = requested_scope.split_whitespace().collect();
for requested in &requested_scopes { if !original_scopes.contains(requested) { return Err(anyhow::anyhow!("Invalid scope: {}", requested)); } }
requested_scope.clone() } else { grant.scope.clone() };
// Generate new session ID let session_id = Uuid::new_v4().to_string();
// Create new JWT tokens let token_pair = jwt_manager.create_token_pair( grant.user_id, user.username, user.email, roles, permissions, scope.clone(), session_id, Some(client.client_id.clone()), )?;
// Store new access token grant let access_token_hash = self.hash_token(&token_pair.access_token)?; let access_grant = AccessTokenGrant { client_id: client.client_id.clone(), user_id: grant.user_id, scope: scope.clone(), expires_at: Utc::now() + Duration::seconds(client.token_lifetime_seconds as i64), };
self.access_tokens.lock().unwrap().insert(access_token_hash.clone(), access_grant);
// Store new refresh token grant let new_refresh_token_hash = self.hash_token(&token_pair.refresh_token)?; let new_refresh_grant = RefreshTokenGrant { access_token_hash: access_token_hash.clone(), client_id: client.client_id.clone(), user_id: grant.user_id, scope: scope.clone(), expires_at: Utc::now() + Duration::seconds(client.refresh_token_lifetime_seconds as i64), };
self.refresh_tokens.lock().unwrap().insert(new_refresh_token_hash, new_refresh_grant);
Ok(TokenResponse { access_token: token_pair.access_token, token_type: token_pair.token_type, expires_in: token_pair.expires_in, refresh_token: Some(token_pair.refresh_token), scope, id_token: None, }) }
fn generate_authorization_code(&self) -> String { use ring::rand::{SecureRandom, SystemRandom}; let rng = SystemRandom::new(); let mut code_bytes = [0u8; 32]; rng.fill(&mut code_bytes).unwrap(); base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&code_bytes) }
fn verify_pkce_challenge( &self, code_challenge: &str, code_verifier: &str, method: &Option<String>, ) -> Result<bool> { let method = method.as_deref().unwrap_or("plain");
match method { "plain" => Ok(code_challenge == code_verifier), "S256" => { use sha2::{Sha256, Digest}; let mut hasher = Sha256::new(); hasher.update(code_verifier.as_bytes()); let hash = hasher.finalize(); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&hash); Ok(code_challenge == encoded) } _ => Err(anyhow::anyhow!("Unsupported code_challenge_method")), } }
fn verify_client_secret(&self, client: &OAuth2Client, secret: &str) -> Result<bool> { use argon2::{Argon2, PasswordHash, PasswordVerifier};
let parsed_hash = PasswordHash::new(&client.client_secret_hash) .map_err(|e| anyhow::anyhow!("Invalid client secret hash: {}", e))?;
Ok(Argon2::default() .verify_password(secret.as_bytes(), &parsed_hash) .is_ok()) }
fn hash_token(&self, token: &str) -> Result<String> { use sha2::{Sha256, Digest}; let mut hasher = Sha256::new(); hasher.update(token.as_bytes()); Ok(hex::encode(hasher.finalize())) }
fn revoke_access_token(&self, token_hash: &str) -> Result<()> { self.access_tokens.lock().unwrap().remove(token_hash); Ok(()) }
fn revoke_refresh_token(&self, token_hash: &str) -> Result<()> { self.refresh_tokens.lock().unwrap().remove(token_hash); Ok(()) }
pub fn validate_access_token(&self, token: &str) -> Result<Option<AccessTokenInfo>> { let token_hash = self.hash_token(token)?; let grants = self.access_tokens.lock().unwrap();
if let Some(grant) = grants.get(&token_hash) { if Utc::now() <= grant.expires_at { return Ok(Some(AccessTokenInfo { client_id: grant.client_id.clone(), user_id: grant.user_id, scope: grant.scope.clone(), expires_at: grant.expires_at, })); } }
Ok(None) }
pub fn introspect_token(&self, token: &str, client_id: &str) -> Result<TokenIntrospectionResponse> { let token_hash = self.hash_token(token)?; let grants = self.access_tokens.lock().unwrap();
if let Some(grant) = grants.get(&token_hash) { if grant.client_id == client_id && Utc::now() <= grant.expires_at { return Ok(TokenIntrospectionResponse { active: true, client_id: Some(grant.client_id.clone()), username: None, // Would need to fetch from user service scope: Some(grant.scope.clone()), exp: Some(grant.expires_at.timestamp()), iat: None, sub: Some(grant.user_id.to_string()), }); } }
Ok(TokenIntrospectionResponse { active: false, client_id: None, username: None, scope: None, exp: None, iat: None, sub: None, }) }}
#[derive(Debug, Clone)]pub struct AccessTokenInfo { pub client_id: String, pub user_id: Uuid, pub scope: String, pub expires_at: DateTime<Utc>,}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct TokenIntrospectionResponse { pub active: bool, pub client_id: Option<String>, pub username: Option<String>, pub scope: Option<String>, pub exp: Option<i64>, pub iat: Option<i64>, pub sub: Option<String>,}
Testing and Security Validation
Comprehensive Security Tests
#[cfg(test)]mod tests { use super::*; use tempfile::TempDir; use tokio::time::{sleep, Duration};
#[tokio::test] async fn test_jwt_security_vulnerabilities() { let temp_dir = TempDir::new().unwrap(); let config = create_test_config(&temp_dir); let jwt_manager = create_test_jwt_manager();
// Test 1: Ensure tokens expire let token_pair = jwt_manager.create_token_pair( Uuid::new_v4(), "testuser".to_string(), "test@example.com".to_string(), vec!["user".to_string()], vec!["read".to_string()], "read".to_string(), Uuid::new_v4().to_string(), None, ).unwrap();
// Wait for token to expire (in real test, mock time) sleep(Duration::from_secs(2)).await;
let result = jwt_manager.verify_token(&token_pair.access_token); assert!(result.is_err(), "Expired token should be rejected");
// Test 2: Ensure token tampering is detected let mut tampered_token = token_pair.access_token.clone(); tampered_token.push('x'); // Corrupt the token
let result = jwt_manager.verify_token(&tampered_token); assert!(result.is_err(), "Tampered token should be rejected");
// Test 3: Test algorithm confusion attack prevention let malicious_token = create_malicious_none_algorithm_token(); let result = jwt_manager.verify_token(&malicious_token); assert!(result.is_err(), "None algorithm token should be rejected"); }
#[tokio::test] async fn test_mfa_security() { let mfa_manager = create_test_mfa_manager(); let user_id = Uuid::new_v4();
// Setup MFA let setup = mfa_manager.generate_mfa_setup( user_id, "testuser", "test@example.com" ).unwrap();
let config = mfa_manager.enable_mfa( user_id, &setup.secret_key, &setup.backup_codes, ).unwrap();
// Test 1: Invalid TOTP tokens are rejected let invalid_token = "123456"; let result = mfa_manager.verify_totp_token(&config, invalid_token).unwrap(); assert!(!result, "Invalid TOTP token should be rejected");
// Test 2: Replay attacks are prevented let valid_token = generate_valid_totp_token(&setup.secret_key); let first_verify = mfa_manager.verify_totp_token(&config, &valid_token).unwrap(); assert!(first_verify, "Valid TOTP token should be accepted");
// Simulate time passing to prevent replay sleep(Duration::from_secs(31)).await; let replay_verify = mfa_manager.verify_totp_token(&config, &valid_token).unwrap(); assert!(!replay_verify, "Replayed TOTP token should be rejected");
// Test 3: Backup codes can only be used once let mut mutable_config = config.clone(); let backup_code = setup.backup_codes[0].clone();
let first_use = mfa_manager.verify_backup_code(&mut mutable_config, &backup_code).unwrap(); assert!(first_use, "Valid backup code should be accepted");
let second_use = mfa_manager.verify_backup_code(&mut mutable_config, &backup_code).unwrap(); assert!(!second_use, "Used backup code should be rejected"); }
#[tokio::test] async fn test_oauth2_security() { let mut oauth2_provider = OAuth2Provider::new(); let client = create_test_oauth2_client(); oauth2_provider.register_client(client.clone());
let user_id = Uuid::new_v4();
// Test 1: Authorization code can only be used once let auth_request = AuthorizationRequest { response_type: "code".to_string(), client_id: client.client_id.clone(), redirect_uri: client.redirect_uris[0].clone(), scope: Some("read".to_string()), state: Some("test_state".to_string()), code_challenge: None, code_challenge_method: None, };
let auth_response = oauth2_provider.authorize(&auth_request, user_id).unwrap();
// First token exchange should succeed let token_request = TokenRequest { grant_type: "authorization_code".to_string(), code: Some(auth_response.code.clone()), redirect_uri: Some(client.redirect_uris[0].clone()), client_id: client.client_id.clone(), client_secret: Some("test-secret".to_string()), code_verifier: None, refresh_token: None, scope: None, };
let jwt_manager = create_test_jwt_manager(); let user_service = create_test_user_service();
let first_exchange = oauth2_provider.exchange_code_for_token( &token_request, &jwt_manager, &user_service, ); assert!(first_exchange.is_ok(), "First code exchange should succeed");
// Second exchange with same code should fail let second_exchange = oauth2_provider.exchange_code_for_token( &token_request, &jwt_manager, &user_service, ); assert!(second_exchange.is_err(), "Second code exchange should fail");
// Test 2: PKCE validation let pkce_verifier = "test-code-verifier-that-is-long-enough"; let pkce_challenge = generate_pkce_challenge(pkce_verifier);
let auth_request_pkce = AuthorizationRequest { response_type: "code".to_string(), client_id: client.client_id.clone(), redirect_uri: client.redirect_uris[0].clone(), scope: Some("read".to_string()), state: Some("test_state".to_string()), code_challenge: Some(pkce_challenge), code_challenge_method: Some("S256".to_string()), };
let auth_response_pkce = oauth2_provider.authorize(&auth_request_pkce, user_id).unwrap();
// Token exchange with wrong verifier should fail let wrong_verifier_request = TokenRequest { grant_type: "authorization_code".to_string(), code: Some(auth_response_pkce.code.clone()), redirect_uri: Some(client.redirect_uris[0].clone()), client_id: client.client_id.clone(), client_secret: Some("test-secret".to_string()), code_verifier: Some("wrong-verifier".to_string()), refresh_token: None, scope: None, };
let wrong_verifier_result = oauth2_provider.exchange_code_for_token( &wrong_verifier_request, &jwt_manager, &user_service, ); assert!(wrong_verifier_result.is_err(), "Wrong PKCE verifier should be rejected"); }
#[tokio::test] async fn test_rate_limiting() { let rate_limiter = create_test_rate_limiter().await; let user_id = Uuid::new_v4();
// Test multiple failed login attempts for i in 0..6 { let allowed = rate_limiter.check_rate_limit(user_id).await.unwrap();
if i < 5 { assert!(allowed, "First 5 attempts should be allowed"); rate_limiter.record_attempt(user_id, false).await.unwrap(); } else { assert!(!allowed, "6th attempt should be rate limited"); } }
// Test that successful login resets the counter let user_id2 = Uuid::new_v4(); for _ in 0..3 { let allowed = rate_limiter.check_rate_limit(user_id2).await.unwrap(); assert!(allowed); rate_limiter.record_attempt(user_id2, false).await.unwrap(); }
// Successful login should reset rate_limiter.record_attempt(user_id2, true).await.unwrap();
let allowed = rate_limiter.check_rate_limit(user_id2).await.unwrap(); assert!(allowed, "Successful login should reset rate limit"); }
#[tokio::test] async fn test_session_security() { let session_store = create_test_session_store().await; let user_id = Uuid::new_v4();
// Create session let session_id = session_store.create_session( user_id, ClientInfo { ip_address: "192.168.1.1".parse().unwrap(), user_agent: "Test Agent".to_string(), }, Duration::hours(1), ).await.unwrap();
// Test 1: Valid session should be retrievable let session = session_store.get_session(&session_id).await.unwrap(); assert!(session.is_some());
// Test 2: Expired sessions should be invalid session_store.expire_session(&session_id).await.unwrap(); let expired_session = session_store.get_session(&session_id).await.unwrap(); assert!(expired_session.is_none(), "Expired session should not be retrievable");
// Test 3: Session fixation protection let old_session_id = session_store.create_session( user_id, ClientInfo { ip_address: "192.168.1.1".parse().unwrap(), user_agent: "Test Agent".to_string(), }, Duration::hours(1), ).await.unwrap();
let new_session_id = session_store.regenerate_session_id(&old_session_id).await.unwrap();
// Old session should be invalid let old_session = session_store.get_session(&old_session_id).await.unwrap(); assert!(old_session.is_none(), "Old session should be invalidated");
// New session should be valid let new_session = session_store.get_session(&new_session_id).await.unwrap(); assert!(new_session.is_some(), "New session should be valid"); }
#[tokio::test] async fn test_password_security() { let user_service = create_test_user_service();
// Test 1: Weak passwords are rejected let weak_passwords = vec![ "123456", "password", "qwerty", "abc123", "password123", ];
for weak_password in weak_passwords { let result = user_service.validate_password_strength(weak_password); assert!(result.is_err(), "Weak password '{}' should be rejected", weak_password); }
// Test 2: Strong passwords are accepted let strong_password = "MyS3cur3P@ssw0rd!2024"; let result = user_service.validate_password_strength(strong_password); assert!(result.is_ok(), "Strong password should be accepted");
// Test 3: Password hashing is secure let password = "test-password-123"; let hash1 = user_service.hash_password(password).unwrap(); let hash2 = user_service.hash_password(password).unwrap();
// Same password should produce different hashes (salt) assert_ne!(hash1, hash2, "Password hashes should be different due to salt");
// Both hashes should verify correctly assert!(user_service.verify_password(password, &hash1).unwrap()); assert!(user_service.verify_password(password, &hash2).unwrap());
// Wrong password should not verify assert!(!user_service.verify_password("wrong-password", &hash1).unwrap()); }
// Helper functions for tests fn create_test_jwt_manager() -> JwtManager { let secret = b"test-secret-key-that-is-long-enough-for-testing"; let issuer = "test-issuer".to_string(); let mut audience = HashSet::new(); audience.insert("test-audience".to_string());
JwtManager::new( secret, issuer, audience, Duration::seconds(1), // Short expiry for testing Duration::hours(1), ).unwrap() }
fn create_test_mfa_manager() -> MfaManager { let encryption_key = [0u8; 32]; // In real tests, use proper key MfaManager::new(encryption_key, "Test App".to_string()) }
fn create_test_oauth2_client() -> OAuth2Client { OAuth2Client { client_id: "test-client".to_string(), client_secret_hash: hash_client_secret("test-secret"), client_name: "Test Client".to_string(), redirect_uris: vec!["https://example.com/callback".to_string()], allowed_scopes: vec!["read".to_string(), "write".to_string()], is_confidential: true, token_lifetime_seconds: 3600, refresh_token_lifetime_seconds: 86400, } }
async fn create_test_rate_limiter() -> MfaRateLimiter { // Use Redis test instance or mock MfaRateLimiter::new("redis://localhost:6379").unwrap() }
async fn create_test_session_store() -> SessionStore { // Create test session store SessionStore::new("redis://localhost:6379").await.unwrap() }
fn create_test_user_service() -> UserService { // Create test user service with in-memory database UserService::new_with_test_db() }}
Perfect! I’ve created comprehensive blog posts covering the complete Rust security tooling development journey. Let me continue with the next blog post to complete the series.
<function_calls>