elif-auth 0.4.1

Authentication and authorization system for elif.rs framework - JWT, sessions, RBAC, password hashing, and middleware
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
//! Authentication configuration types and utilities

use serde::{Deserialize, Serialize};

/// Main authentication configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuthConfig {
    /// JWT configuration
    pub jwt: JwtConfig,

    /// Session configuration
    pub session: SessionConfig,

    /// Password policy configuration
    pub password: PasswordConfig,

    /// Multi-factor authentication configuration
    pub mfa: MfaConfig,

    /// Rate limiting for authentication attempts
    pub rate_limit: AuthRateLimitConfig,
}

/// JWT token configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtConfig {
    /// Secret key for JWT signing (HS256) or path to private key (RS256)
    pub secret: String,

    /// JWT signing algorithm (HS256, HS384, HS512, RS256, RS384, RS512)
    #[serde(default = "default_jwt_algorithm")]
    pub algorithm: String,

    /// Access token expiration time in seconds
    #[serde(default = "default_access_token_expiry")]
    pub access_token_expiry: u64,

    /// Refresh token expiration time in seconds  
    #[serde(default = "default_refresh_token_expiry")]
    pub refresh_token_expiry: u64,

    /// JWT issuer
    #[serde(default = "default_jwt_issuer")]
    pub issuer: String,

    /// JWT audience
    pub audience: Option<String>,

    /// Allow token refresh
    #[serde(default = "default_true")]
    pub allow_refresh: bool,
}

/// Session configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionConfig {
    /// Session storage backend (memory, database, redis)
    #[serde(default = "default_session_storage")]
    pub storage: String,

    /// Session expiration time in seconds
    #[serde(default = "default_session_expiry")]
    pub expiry: u64,

    /// Session cookie name
    #[serde(default = "default_session_cookie_name")]
    pub cookie_name: String,

    /// Session cookie domain
    pub cookie_domain: Option<String>,

    /// Session cookie path
    #[serde(default = "default_session_cookie_path")]
    pub cookie_path: String,

    /// Session cookie secure flag
    #[serde(default = "default_false")]
    pub cookie_secure: bool,

    /// Session cookie HTTP-only flag
    #[serde(default = "default_true")]
    pub cookie_http_only: bool,

    /// Session cookie SameSite policy
    #[serde(default = "default_session_cookie_same_site")]
    pub cookie_same_site: String,

    /// Session cleanup interval in seconds
    #[serde(default = "default_session_cleanup_interval")]
    pub cleanup_interval: u64,
}

/// Password policy configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PasswordConfig {
    /// Minimum password length
    #[serde(default = "default_min_password_length")]
    pub min_length: usize,

    /// Maximum password length
    #[serde(default = "default_max_password_length")]
    pub max_length: usize,

    /// Require uppercase letters
    #[serde(default = "default_true")]
    pub require_uppercase: bool,

    /// Require lowercase letters  
    #[serde(default = "default_true")]
    pub require_lowercase: bool,

    /// Require numbers
    #[serde(default = "default_true")]
    pub require_numbers: bool,

    /// Require special characters
    #[serde(default = "default_false")]
    pub require_special: bool,

    /// Password hashing algorithm (argon2, bcrypt)
    #[serde(default = "default_hash_algorithm")]
    pub hash_algorithm: String,

    /// Bcrypt cost factor (if using bcrypt)
    #[serde(default = "default_bcrypt_cost")]
    pub bcrypt_cost: u32,

    /// Argon2 memory cost in KB (if using argon2)
    #[serde(default = "default_argon2_memory")]
    pub argon2_memory: u32,

    /// Argon2 time cost (iterations)
    #[serde(default = "default_argon2_iterations")]
    pub argon2_iterations: u32,

    /// Argon2 parallelism factor
    #[serde(default = "default_argon2_parallelism")]
    pub argon2_parallelism: u32,
}

/// Multi-factor authentication configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaConfig {
    /// Enable MFA
    #[serde(default = "default_false")]
    pub enabled: bool,

    /// TOTP issuer name
    #[serde(default = "default_totp_issuer")]
    pub totp_issuer: String,

    /// TOTP time step in seconds
    #[serde(default = "default_totp_step")]
    pub totp_step: u64,

    /// TOTP code length
    #[serde(default = "default_totp_digits")]
    pub totp_digits: usize,

    /// TOTP time window tolerance
    #[serde(default = "default_totp_window")]
    pub totp_window: u8,

    /// Number of backup codes to generate
    #[serde(default = "default_backup_codes_count")]
    pub backup_codes_count: usize,

    /// Backup code length
    #[serde(default = "default_backup_code_length")]
    pub backup_code_length: usize,
}

/// Authentication rate limiting configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthRateLimitConfig {
    /// Maximum login attempts per IP
    #[serde(default = "default_max_attempts")]
    pub max_attempts: u32,

    /// Time window for rate limiting in seconds
    #[serde(default = "default_rate_limit_window")]
    pub window_seconds: u64,

    /// Lockout duration in seconds after max attempts
    #[serde(default = "default_lockout_duration")]
    pub lockout_duration: u64,
}

// Default value functions
fn default_jwt_algorithm() -> String {
    "HS256".to_string()
}
fn default_access_token_expiry() -> u64 {
    15 * 60
} // 15 minutes
fn default_refresh_token_expiry() -> u64 {
    7 * 24 * 60 * 60
} // 7 days
fn default_jwt_issuer() -> String {
    "elif.rs".to_string()
}
fn default_session_storage() -> String {
    "memory".to_string()
}
fn default_session_expiry() -> u64 {
    24 * 60 * 60
} // 24 hours
fn default_session_cookie_name() -> String {
    "elif_session".to_string()
}
fn default_session_cookie_path() -> String {
    "/".to_string()
}
fn default_session_cookie_same_site() -> String {
    "Lax".to_string()
}
fn default_session_cleanup_interval() -> u64 {
    60 * 60
} // 1 hour
fn default_min_password_length() -> usize {
    8
}
fn default_max_password_length() -> usize {
    128
}
fn default_hash_algorithm() -> String {
    "argon2".to_string()
}
fn default_bcrypt_cost() -> u32 {
    12
}
fn default_argon2_memory() -> u32 {
    65536
} // 64MB
fn default_argon2_iterations() -> u32 {
    3
}
fn default_argon2_parallelism() -> u32 {
    4
}
fn default_totp_issuer() -> String {
    "elif.rs".to_string()
}
fn default_totp_step() -> u64 {
    30
}
fn default_totp_digits() -> usize {
    6
}
fn default_totp_window() -> u8 {
    1
}
fn default_backup_codes_count() -> usize {
    10
}
fn default_backup_code_length() -> usize {
    8
}
fn default_max_attempts() -> u32 {
    5
}
fn default_rate_limit_window() -> u64 {
    15 * 60
} // 15 minutes
fn default_lockout_duration() -> u64 {
    30 * 60
} // 30 minutes
fn default_true() -> bool {
    true
}
fn default_false() -> bool {
    false
}

impl Default for JwtConfig {
    fn default() -> Self {
        Self {
            secret: "default-secret-key-change-in-production-32-chars-long".to_string(), // 32+ chars for validation
            algorithm: default_jwt_algorithm(),
            access_token_expiry: default_access_token_expiry(),
            refresh_token_expiry: default_refresh_token_expiry(),
            issuer: default_jwt_issuer(),
            audience: None,
            allow_refresh: default_true(),
        }
    }
}

impl Default for SessionConfig {
    fn default() -> Self {
        Self {
            storage: default_session_storage(),
            expiry: default_session_expiry(),
            cookie_name: default_session_cookie_name(),
            cookie_domain: None,
            cookie_path: default_session_cookie_path(),
            cookie_secure: default_false(),
            cookie_http_only: default_true(),
            cookie_same_site: default_session_cookie_same_site(),
            cleanup_interval: default_session_cleanup_interval(),
        }
    }
}

impl Default for PasswordConfig {
    fn default() -> Self {
        Self {
            min_length: default_min_password_length(),
            max_length: default_max_password_length(),
            require_uppercase: default_true(),
            require_lowercase: default_true(),
            require_numbers: default_true(),
            require_special: default_false(),
            hash_algorithm: default_hash_algorithm(),
            bcrypt_cost: default_bcrypt_cost(),
            argon2_memory: default_argon2_memory(),
            argon2_iterations: default_argon2_iterations(),
            argon2_parallelism: default_argon2_parallelism(),
        }
    }
}

impl Default for MfaConfig {
    fn default() -> Self {
        Self {
            enabled: default_false(),
            totp_issuer: default_totp_issuer(),
            totp_step: default_totp_step(),
            totp_digits: default_totp_digits(),
            totp_window: default_totp_window(),
            backup_codes_count: default_backup_codes_count(),
            backup_code_length: default_backup_code_length(),
        }
    }
}

impl Default for AuthRateLimitConfig {
    fn default() -> Self {
        Self {
            max_attempts: default_max_attempts(),
            window_seconds: default_rate_limit_window(),
            lockout_duration: default_lockout_duration(),
        }
    }
}

impl AuthConfig {
    /// Create a development configuration with secure defaults
    pub fn development() -> Self {
        let mut config = Self::default();
        config.jwt.secret = "dev-secret-key-change-in-production".to_string();
        config.session.cookie_secure = false; // Allow HTTP in development
        config.password.require_special = false; // Relaxed for dev
        config
    }

    /// Create a production configuration with strict security
    pub fn production() -> Self {
        let mut config = Self::default();
        config.session.cookie_secure = true;
        config.session.cookie_same_site = "Strict".to_string();
        config.password.require_special = true;
        config.password.min_length = 12;
        config.mfa.enabled = true;
        config
    }

    /// Validate the configuration
    pub fn validate(&self) -> Result<(), String> {
        // Validate JWT configuration
        if self.jwt.secret.len() < 32 {
            return Err("JWT secret must be at least 32 characters".to_string());
        }

        if !["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"]
            .contains(&self.jwt.algorithm.as_str())
        {
            return Err("Invalid JWT algorithm".to_string());
        }

        // Validate password policy
        if self.password.min_length > self.password.max_length {
            return Err("Password min_length cannot be greater than max_length".to_string());
        }

        if self.password.min_length < 1 {
            return Err("Password min_length must be at least 1".to_string());
        }

        // Validate session configuration
        if !["memory", "database", "redis"].contains(&self.session.storage.as_str()) {
            return Err("Invalid session storage backend".to_string());
        }

        if !["Strict", "Lax", "None"].contains(&self.session.cookie_same_site.as_str()) {
            return Err("Invalid session cookie SameSite policy".to_string());
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = AuthConfig::default();
        assert_eq!(config.jwt.algorithm, "HS256");
        assert_eq!(config.session.storage, "memory");
        assert_eq!(config.password.hash_algorithm, "argon2");
        assert!(!config.mfa.enabled);
    }

    #[test]
    fn test_development_config() {
        let config = AuthConfig::development();
        assert!(!config.session.cookie_secure);
        assert!(!config.password.require_special);
        assert_eq!(config.jwt.secret, "dev-secret-key-change-in-production");
    }

    #[test]
    fn test_production_config() {
        let config = AuthConfig::production();
        assert!(config.session.cookie_secure);
        assert!(config.password.require_special);
        assert_eq!(config.password.min_length, 12);
        assert!(config.mfa.enabled);
        assert_eq!(config.session.cookie_same_site, "Strict");
    }

    #[test]
    fn test_config_validation() {
        let mut config = AuthConfig::default();
        assert!(config.validate().is_ok());

        // Test invalid JWT secret
        config.jwt.secret = "short".to_string();
        assert!(config.validate().is_err());

        // Test invalid JWT algorithm
        config.jwt.secret = "long-enough-secret-key-for-validation".to_string();
        config.jwt.algorithm = "INVALID".to_string();
        assert!(config.validate().is_err());

        // Test invalid password policy
        config.jwt.algorithm = "HS256".to_string();
        config.password.min_length = 20;
        config.password.max_length = 10;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_durations() {
        let config = AuthConfig::default();
        assert_eq!(config.jwt.access_token_expiry, 15 * 60); // 15 minutes
        assert_eq!(config.jwt.refresh_token_expiry, 7 * 24 * 60 * 60); // 7 days
        assert_eq!(config.session.expiry, 24 * 60 * 60); // 24 hours
    }
}