auth_framework/utils/
validation.rs1use crate::errors::{AuthError, Result};
7use regex::Regex;
8use std::collections::HashSet;
9
10#[derive(Debug, Clone)]
12pub struct PasswordPolicy {
13 pub min_length: usize,
15 pub max_length: usize,
17 pub require_uppercase: bool,
19 pub require_lowercase: bool,
21 pub require_digit: bool,
23 pub require_special: bool,
25 pub banned_passwords: HashSet<String>,
27 pub min_entropy: f64,
29}
30
31impl Default for PasswordPolicy {
32 fn default() -> Self {
33 let mut banned_passwords = HashSet::new();
34 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
67pub fn validate_password_enhanced(password: &str, policy: &PasswordPolicy) -> Result<()> {
69 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 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 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 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
128pub fn validate_password(password: &str) -> Result<()> {
130 validate_password_enhanced(password, &PasswordPolicy::default())
131}
132
133fn 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
152pub 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 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 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
190pub 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 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
211pub 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 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 assert!(validate_password_enhanced("StrongP@ssw0rd!", &policy).is_ok());
250
251 assert!(validate_password_enhanced("Short1!", &policy).is_err());
253
254 assert!(validate_password_enhanced("lowercase123!", &policy).is_err());
256
257 assert!(validate_password_enhanced("UPPERCASE123!", &policy).is_err());
259
260 assert!(validate_password_enhanced("NoDigitPass!", &policy).is_err());
262
263 assert!(validate_password_enhanced("NoSpecialChar123", &policy).is_err());
265
266 assert!(validate_password_enhanced("password", &policy).is_err());
268 }
269
270 #[test]
271 fn test_username_validation() {
272 assert!(validate_username("validuser").is_ok());
274 assert!(validate_username("user_123").is_ok());
275 assert!(validate_username("test-user").is_ok());
276
277 assert!(validate_username("").is_err()); assert!(validate_username("ab").is_err()); assert!(validate_username("123user").is_err()); assert!(validate_username("user@test").is_err()); }
283
284 #[test]
285 fn test_email_validation() {
286 assert!(validate_email("test@example.com").is_ok());
288 assert!(validate_email("user.name+tag@domain.co.uk").is_ok());
289
290 assert!(validate_email("").is_err()); assert!(validate_email("invalid.email").is_err()); assert!(validate_email("@domain.com").is_err()); assert!(validate_email("test@").is_err()); }
296}