1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7pub struct AuthConfig {
8 pub jwt: JwtConfig,
10
11 pub session: SessionConfig,
13
14 pub password: PasswordConfig,
16
17 pub mfa: MfaConfig,
19
20 pub rate_limit: AuthRateLimitConfig,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct JwtConfig {
27 pub secret: String,
29
30 #[serde(default = "default_jwt_algorithm")]
32 pub algorithm: String,
33
34 #[serde(default = "default_access_token_expiry")]
36 pub access_token_expiry: u64,
37
38 #[serde(default = "default_refresh_token_expiry")]
40 pub refresh_token_expiry: u64,
41
42 #[serde(default = "default_jwt_issuer")]
44 pub issuer: String,
45
46 pub audience: Option<String>,
48
49 #[serde(default = "default_true")]
51 pub allow_refresh: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SessionConfig {
57 #[serde(default = "default_session_storage")]
59 pub storage: String,
60
61 #[serde(default = "default_session_expiry")]
63 pub expiry: u64,
64
65 #[serde(default = "default_session_cookie_name")]
67 pub cookie_name: String,
68
69 pub cookie_domain: Option<String>,
71
72 #[serde(default = "default_session_cookie_path")]
74 pub cookie_path: String,
75
76 #[serde(default = "default_false")]
78 pub cookie_secure: bool,
79
80 #[serde(default = "default_true")]
82 pub cookie_http_only: bool,
83
84 #[serde(default = "default_session_cookie_same_site")]
86 pub cookie_same_site: String,
87
88 #[serde(default = "default_session_cleanup_interval")]
90 pub cleanup_interval: u64,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct PasswordConfig {
96 #[serde(default = "default_min_password_length")]
98 pub min_length: usize,
99
100 #[serde(default = "default_max_password_length")]
102 pub max_length: usize,
103
104 #[serde(default = "default_true")]
106 pub require_uppercase: bool,
107
108 #[serde(default = "default_true")]
110 pub require_lowercase: bool,
111
112 #[serde(default = "default_true")]
114 pub require_numbers: bool,
115
116 #[serde(default = "default_false")]
118 pub require_special: bool,
119
120 #[serde(default = "default_hash_algorithm")]
122 pub hash_algorithm: String,
123
124 #[serde(default = "default_bcrypt_cost")]
126 pub bcrypt_cost: u32,
127
128 #[serde(default = "default_argon2_memory")]
130 pub argon2_memory: u32,
131
132 #[serde(default = "default_argon2_iterations")]
134 pub argon2_iterations: u32,
135
136 #[serde(default = "default_argon2_parallelism")]
138 pub argon2_parallelism: u32,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct MfaConfig {
144 #[serde(default = "default_false")]
146 pub enabled: bool,
147
148 #[serde(default = "default_totp_issuer")]
150 pub totp_issuer: String,
151
152 #[serde(default = "default_totp_step")]
154 pub totp_step: u64,
155
156 #[serde(default = "default_totp_digits")]
158 pub totp_digits: usize,
159
160 #[serde(default = "default_totp_window")]
162 pub totp_window: u8,
163
164 #[serde(default = "default_backup_codes_count")]
166 pub backup_codes_count: usize,
167
168 #[serde(default = "default_backup_code_length")]
170 pub backup_code_length: usize,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct AuthRateLimitConfig {
176 #[serde(default = "default_max_attempts")]
178 pub max_attempts: u32,
179
180 #[serde(default = "default_rate_limit_window")]
182 pub window_seconds: u64,
183
184 #[serde(default = "default_lockout_duration")]
186 pub lockout_duration: u64,
187}
188
189fn default_jwt_algorithm() -> String {
191 "HS256".to_string()
192}
193fn default_access_token_expiry() -> u64 {
194 15 * 60
195} fn default_refresh_token_expiry() -> u64 {
197 7 * 24 * 60 * 60
198} fn default_jwt_issuer() -> String {
200 "elif.rs".to_string()
201}
202fn default_session_storage() -> String {
203 "memory".to_string()
204}
205fn default_session_expiry() -> u64 {
206 24 * 60 * 60
207} fn default_session_cookie_name() -> String {
209 "elif_session".to_string()
210}
211fn default_session_cookie_path() -> String {
212 "/".to_string()
213}
214fn default_session_cookie_same_site() -> String {
215 "Lax".to_string()
216}
217fn default_session_cleanup_interval() -> u64 {
218 60 * 60
219} fn default_min_password_length() -> usize {
221 8
222}
223fn default_max_password_length() -> usize {
224 128
225}
226fn default_hash_algorithm() -> String {
227 "argon2".to_string()
228}
229fn default_bcrypt_cost() -> u32 {
230 12
231}
232fn default_argon2_memory() -> u32 {
233 65536
234} fn default_argon2_iterations() -> u32 {
236 3
237}
238fn default_argon2_parallelism() -> u32 {
239 4
240}
241fn default_totp_issuer() -> String {
242 "elif.rs".to_string()
243}
244fn default_totp_step() -> u64 {
245 30
246}
247fn default_totp_digits() -> usize {
248 6
249}
250fn default_totp_window() -> u8 {
251 1
252}
253fn default_backup_codes_count() -> usize {
254 10
255}
256fn default_backup_code_length() -> usize {
257 8
258}
259fn default_max_attempts() -> u32 {
260 5
261}
262fn default_rate_limit_window() -> u64 {
263 15 * 60
264} fn default_lockout_duration() -> u64 {
266 30 * 60
267} fn default_true() -> bool {
269 true
270}
271fn default_false() -> bool {
272 false
273}
274
275impl Default for JwtConfig {
276 fn default() -> Self {
277 Self {
278 secret: "default-secret-key-change-in-production-32-chars-long".to_string(), algorithm: default_jwt_algorithm(),
280 access_token_expiry: default_access_token_expiry(),
281 refresh_token_expiry: default_refresh_token_expiry(),
282 issuer: default_jwt_issuer(),
283 audience: None,
284 allow_refresh: default_true(),
285 }
286 }
287}
288
289impl Default for SessionConfig {
290 fn default() -> Self {
291 Self {
292 storage: default_session_storage(),
293 expiry: default_session_expiry(),
294 cookie_name: default_session_cookie_name(),
295 cookie_domain: None,
296 cookie_path: default_session_cookie_path(),
297 cookie_secure: default_false(),
298 cookie_http_only: default_true(),
299 cookie_same_site: default_session_cookie_same_site(),
300 cleanup_interval: default_session_cleanup_interval(),
301 }
302 }
303}
304
305impl Default for PasswordConfig {
306 fn default() -> Self {
307 Self {
308 min_length: default_min_password_length(),
309 max_length: default_max_password_length(),
310 require_uppercase: default_true(),
311 require_lowercase: default_true(),
312 require_numbers: default_true(),
313 require_special: default_false(),
314 hash_algorithm: default_hash_algorithm(),
315 bcrypt_cost: default_bcrypt_cost(),
316 argon2_memory: default_argon2_memory(),
317 argon2_iterations: default_argon2_iterations(),
318 argon2_parallelism: default_argon2_parallelism(),
319 }
320 }
321}
322
323impl Default for MfaConfig {
324 fn default() -> Self {
325 Self {
326 enabled: default_false(),
327 totp_issuer: default_totp_issuer(),
328 totp_step: default_totp_step(),
329 totp_digits: default_totp_digits(),
330 totp_window: default_totp_window(),
331 backup_codes_count: default_backup_codes_count(),
332 backup_code_length: default_backup_code_length(),
333 }
334 }
335}
336
337impl Default for AuthRateLimitConfig {
338 fn default() -> Self {
339 Self {
340 max_attempts: default_max_attempts(),
341 window_seconds: default_rate_limit_window(),
342 lockout_duration: default_lockout_duration(),
343 }
344 }
345}
346
347impl AuthConfig {
348 pub fn development() -> Self {
350 let mut config = Self::default();
351 config.jwt.secret = "dev-secret-key-change-in-production".to_string();
352 config.session.cookie_secure = false; config.password.require_special = false; config
355 }
356
357 pub fn production() -> Self {
359 let mut config = Self::default();
360 config.session.cookie_secure = true;
361 config.session.cookie_same_site = "Strict".to_string();
362 config.password.require_special = true;
363 config.password.min_length = 12;
364 config.mfa.enabled = true;
365 config
366 }
367
368 pub fn validate(&self) -> Result<(), String> {
370 if self.jwt.secret.len() < 32 {
372 return Err("JWT secret must be at least 32 characters".to_string());
373 }
374
375 if !["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"]
376 .contains(&self.jwt.algorithm.as_str())
377 {
378 return Err("Invalid JWT algorithm".to_string());
379 }
380
381 if self.password.min_length > self.password.max_length {
383 return Err("Password min_length cannot be greater than max_length".to_string());
384 }
385
386 if self.password.min_length < 1 {
387 return Err("Password min_length must be at least 1".to_string());
388 }
389
390 if !["memory", "database", "redis"].contains(&self.session.storage.as_str()) {
392 return Err("Invalid session storage backend".to_string());
393 }
394
395 if !["Strict", "Lax", "None"].contains(&self.session.cookie_same_site.as_str()) {
396 return Err("Invalid session cookie SameSite policy".to_string());
397 }
398
399 Ok(())
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_default_config() {
409 let config = AuthConfig::default();
410 assert_eq!(config.jwt.algorithm, "HS256");
411 assert_eq!(config.session.storage, "memory");
412 assert_eq!(config.password.hash_algorithm, "argon2");
413 assert!(!config.mfa.enabled);
414 }
415
416 #[test]
417 fn test_development_config() {
418 let config = AuthConfig::development();
419 assert!(!config.session.cookie_secure);
420 assert!(!config.password.require_special);
421 assert_eq!(config.jwt.secret, "dev-secret-key-change-in-production");
422 }
423
424 #[test]
425 fn test_production_config() {
426 let config = AuthConfig::production();
427 assert!(config.session.cookie_secure);
428 assert!(config.password.require_special);
429 assert_eq!(config.password.min_length, 12);
430 assert!(config.mfa.enabled);
431 assert_eq!(config.session.cookie_same_site, "Strict");
432 }
433
434 #[test]
435 fn test_config_validation() {
436 let mut config = AuthConfig::default();
437 assert!(config.validate().is_ok());
438
439 config.jwt.secret = "short".to_string();
441 assert!(config.validate().is_err());
442
443 config.jwt.secret = "long-enough-secret-key-for-validation".to_string();
445 config.jwt.algorithm = "INVALID".to_string();
446 assert!(config.validate().is_err());
447
448 config.jwt.algorithm = "HS256".to_string();
450 config.password.min_length = 20;
451 config.password.max_length = 10;
452 assert!(config.validate().is_err());
453 }
454
455 #[test]
456 fn test_durations() {
457 let config = AuthConfig::default();
458 assert_eq!(config.jwt.access_token_expiry, 15 * 60); assert_eq!(config.jwt.refresh_token_expiry, 7 * 24 * 60 * 60); assert_eq!(config.session.expiry, 24 * 60 * 60); }
462}