auth_framework/utils/
validation.rs

1//! Validation utilities for the AuthFramework.
2//!
3//! This module provides comprehensive input validation functions for
4//! authentication-related data including passwords, usernames, emails, and more.
5
6use crate::errors::{AuthError, Result};
7use regex::Regex;
8use std::collections::HashSet;
9
10/// Enhanced password validation configuration
11#[derive(Debug, Clone)]
12pub struct PasswordPolicy {
13    /// Minimum password length
14    pub min_length: usize,
15    /// Maximum password length  
16    pub max_length: usize,
17    /// Require at least one uppercase letter
18    pub require_uppercase: bool,
19    /// Require at least one lowercase letter
20    pub require_lowercase: bool,
21    /// Require at least one digit
22    pub require_digit: bool,
23    /// Require at least one special character
24    pub require_special: bool,
25    /// List of banned common passwords
26    pub banned_passwords: HashSet<String>,
27    /// Minimum entropy requirement
28    pub min_entropy: f64,
29}
30
31impl Default for PasswordPolicy {
32    fn default() -> Self {
33        let mut banned_passwords = HashSet::new();
34        // Add common weak passwords
35        for password in [
36            "password",
37            "123456",
38            "password123",
39            "admin",
40            "qwerty",
41            "letmein",
42            "welcome",
43            "monkey",
44            "dragon",
45            "password1",
46            "123456789",
47            "1234567890",
48            "abc123",
49            "iloveyou",
50        ] {
51            banned_passwords.insert(password.to_string());
52        }
53
54        Self {
55            min_length: 8,
56            max_length: 128,
57            require_uppercase: true,
58            require_lowercase: true,
59            require_digit: true,
60            require_special: true,
61            banned_passwords,
62            min_entropy: 3.0,
63        }
64    }
65}
66
67/// Enhanced password validation with configurable policy
68pub fn validate_password_enhanced(password: &str, policy: &PasswordPolicy) -> Result<()> {
69    // Check length requirements
70    if password.len() < policy.min_length {
71        return Err(AuthError::validation(format!(
72            "Password must be at least {} characters long",
73            policy.min_length
74        )));
75    }
76
77    if password.len() > policy.max_length {
78        return Err(AuthError::validation(format!(
79            "Password must be no more than {} characters long",
80            policy.max_length
81        )));
82    }
83
84    // Check character requirements
85    if policy.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
86        return Err(AuthError::validation(
87            "Password must contain at least one uppercase letter".to_string(),
88        ));
89    }
90
91    if policy.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
92        return Err(AuthError::validation(
93            "Password must contain at least one lowercase letter".to_string(),
94        ));
95    }
96
97    if policy.require_digit && !password.chars().any(|c| c.is_numeric()) {
98        return Err(AuthError::validation(
99            "Password must contain at least one digit".to_string(),
100        ));
101    }
102
103    if policy.require_special && !password.chars().any(|c| !c.is_alphanumeric()) {
104        return Err(AuthError::validation(
105            "Password must contain at least one special character".to_string(),
106        ));
107    }
108
109    // Check against banned passwords
110    if policy.banned_passwords.contains(&password.to_lowercase()) {
111        return Err(AuthError::validation(
112            "Password is too common and not allowed".to_string(),
113        ));
114    }
115
116    // Calculate entropy and check minimum requirement
117    let entropy = calculate_password_entropy(password);
118    if entropy < policy.min_entropy {
119        return Err(AuthError::validation(format!(
120            "Password entropy ({:.2}) is below minimum requirement ({:.2})",
121            entropy, policy.min_entropy
122        )));
123    }
124
125    Ok(())
126}
127
128/// Simple password validation with default policy
129pub fn validate_password(password: &str) -> Result<()> {
130    validate_password_enhanced(password, &PasswordPolicy::default())
131}
132
133/// Calculate password entropy using Shannon entropy formula
134fn calculate_password_entropy(password: &str) -> f64 {
135    let mut char_counts = std::collections::HashMap::new();
136
137    for c in password.chars() {
138        *char_counts.entry(c).or_insert(0) += 1;
139    }
140
141    let length = password.len() as f64;
142    let mut entropy = 0.0;
143
144    for &count in char_counts.values() {
145        let probability = count as f64 / length;
146        entropy -= probability * probability.log2();
147    }
148
149    entropy
150}
151
152/// Validate username format
153pub fn validate_username(username: &str) -> Result<()> {
154    if username.is_empty() {
155        return Err(AuthError::validation(
156            "Username cannot be empty".to_string(),
157        ));
158    }
159
160    if username.len() < 3 {
161        return Err(AuthError::validation(
162            "Username must be at least 3 characters long".to_string(),
163        ));
164    }
165
166    if username.len() > 50 {
167        return Err(AuthError::validation(
168            "Username must be no more than 50 characters long".to_string(),
169        ));
170    }
171
172    // Username can contain letters, numbers, underscores, and hyphens
173    let username_regex = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
174    if !username_regex.is_match(username) {
175        return Err(AuthError::validation(
176            "Username can only contain letters, numbers, underscores, and hyphens".to_string(),
177        ));
178    }
179
180    // Must start with a letter
181    if !username.chars().next().unwrap().is_alphabetic() {
182        return Err(AuthError::validation(
183            "Username must start with a letter".to_string(),
184        ));
185    }
186
187    Ok(())
188}
189
190/// Validate email format
191pub fn validate_email(email: &str) -> Result<()> {
192    if email.is_empty() {
193        return Err(AuthError::validation("Email cannot be empty".to_string()));
194    }
195
196    // Basic email validation regex
197    let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
198    if !email_regex.is_match(email) {
199        return Err(AuthError::validation("Invalid email format".to_string()));
200    }
201
202    if email.len() > 254 {
203        return Err(AuthError::validation(
204            "Email address is too long".to_string(),
205        ));
206    }
207
208    Ok(())
209}
210
211/// Validate API key format
212pub fn validate_api_key(api_key: &str) -> Result<()> {
213    if api_key.is_empty() {
214        return Err(AuthError::validation("API key cannot be empty".to_string()));
215    }
216
217    if api_key.len() < 32 {
218        return Err(AuthError::validation(
219            "API key must be at least 32 characters long".to_string(),
220        ));
221    }
222
223    if api_key.len() > 128 {
224        return Err(AuthError::validation(
225            "API key must be no more than 128 characters long".to_string(),
226        ));
227    }
228
229    // API key should be alphanumeric
230    let api_key_regex = Regex::new(r"^[a-zA-Z0-9]+$").unwrap();
231    if !api_key_regex.is_match(api_key) {
232        return Err(AuthError::validation(
233            "API key can only contain letters and numbers".to_string(),
234        ));
235    }
236
237    Ok(())
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_password_validation() {
246        let policy = PasswordPolicy::default();
247
248        // Valid password
249        assert!(validate_password_enhanced("StrongP@ssw0rd!", &policy).is_ok());
250
251        // Too short
252        assert!(validate_password_enhanced("Short1!", &policy).is_err());
253
254        // No uppercase
255        assert!(validate_password_enhanced("lowercase123!", &policy).is_err());
256
257        // No lowercase
258        assert!(validate_password_enhanced("UPPERCASE123!", &policy).is_err());
259
260        // No digit
261        assert!(validate_password_enhanced("NoDigitPass!", &policy).is_err());
262
263        // No special character
264        assert!(validate_password_enhanced("NoSpecialChar123", &policy).is_err());
265
266        // Banned password
267        assert!(validate_password_enhanced("password", &policy).is_err());
268    }
269
270    #[test]
271    fn test_username_validation() {
272        // Valid usernames
273        assert!(validate_username("validuser").is_ok());
274        assert!(validate_username("user_123").is_ok());
275        assert!(validate_username("test-user").is_ok());
276
277        // Invalid usernames
278        assert!(validate_username("").is_err()); // Empty
279        assert!(validate_username("ab").is_err()); // Too short
280        assert!(validate_username("123user").is_err()); // Starts with number
281        assert!(validate_username("user@test").is_err()); // Invalid character
282    }
283
284    #[test]
285    fn test_email_validation() {
286        // Valid emails
287        assert!(validate_email("test@example.com").is_ok());
288        assert!(validate_email("user.name+tag@domain.co.uk").is_ok());
289
290        // Invalid emails
291        assert!(validate_email("").is_err()); // Empty
292        assert!(validate_email("invalid.email").is_err()); // No @
293        assert!(validate_email("@domain.com").is_err()); // No local part
294        assert!(validate_email("test@").is_err()); // No domain
295    }
296}