Skip to main content

mockforge_vbr/
auth.rs

1//! Authentication emulation
2//!
3//! This module provides virtual user management, JWT token generation/validation,
4//! and session-based authentication for the VBR engine.
5
6use crate::{Error, Result};
7use chrono::Duration;
8use mockforge_core::time_travel_now;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use uuid::Uuid;
12
13/// Virtual user for authentication
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct VirtualUser {
16    /// User ID
17    pub id: Uuid,
18    /// Username
19    pub username: String,
20    /// Email
21    pub email: String,
22    /// Password hash (for virtual users)
23    #[serde(skip_serializing)]
24    pub password_hash: Option<String>,
25    /// User roles/permissions
26    #[serde(default)]
27    pub roles: Vec<String>,
28}
29
30/// JWT claims for VBR authentication
31#[derive(Debug, Serialize, Deserialize)]
32struct JwtClaims {
33    /// Subject (user ID)
34    sub: String,
35    /// Username
36    username: String,
37    /// Email
38    email: String,
39    /// Expiration time (Unix timestamp)
40    exp: usize,
41    /// Issued at (Unix timestamp)
42    iat: usize,
43    /// Roles
44    #[serde(default)]
45    roles: Vec<String>,
46}
47
48impl JwtClaims {
49    /// Check if token is expired
50    ///
51    /// Automatically uses virtual clock if time travel is enabled,
52    /// otherwise uses real time.
53    fn is_expired(&self) -> bool {
54        let now = time_travel_now().timestamp() as usize;
55        now >= self.exp
56    }
57}
58
59/// Authentication service for VBR
60pub struct VbrAuthService {
61    /// JWT secret (required for token generation)
62    jwt_secret: String,
63    /// Token expiration in seconds
64    token_expiration: u64,
65    /// Virtual users (in-memory storage for demo)
66    /// In production, this would be stored in the virtual database
67    users: std::sync::Arc<tokio::sync::RwLock<HashMap<String, VirtualUser>>>,
68}
69
70impl VbrAuthService {
71    /// Create a new authentication service
72    pub fn new(jwt_secret: String, token_expiration_secs: u64) -> Self {
73        Self {
74            jwt_secret,
75            token_expiration: token_expiration_secs,
76            users: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
77        }
78    }
79
80    /// Create a default user (for demo/testing)
81    pub async fn create_default_user(
82        &self,
83        username: String,
84        password: String,
85        email: String,
86    ) -> Result<VirtualUser> {
87        // Hash password (simple implementation - in production use bcrypt)
88        let password_hash = self.hash_password(&password)?;
89
90        let user = VirtualUser {
91            id: Uuid::new_v4(),
92            username: username.clone(),
93            email,
94            password_hash: Some(password_hash),
95            roles: Vec::new(),
96        };
97
98        let mut users = self.users.write().await;
99        users.insert(username, user.clone());
100        Ok(user)
101    }
102
103    /// Hash a password (simple implementation)
104    fn hash_password(&self, password: &str) -> Result<String> {
105        // Simple hash for demo - in production use bcrypt or argon2
106        use sha2::{Digest, Sha256};
107        let mut hasher = Sha256::new();
108        hasher.update(password.as_bytes());
109        hasher.update(self.jwt_secret.as_bytes()); // Salt with JWT secret
110        let hash = hasher.finalize();
111        Ok(format!("{:x}", hash))
112    }
113
114    /// Verify a password
115    fn verify_password(&self, password: &str, hash: &str) -> bool {
116        match self.hash_password(password) {
117            Ok(new_hash) => new_hash == hash,
118            Err(_) => false,
119        }
120    }
121
122    /// Authenticate a user
123    pub async fn authenticate(&self, username: &str, password: &str) -> Result<VirtualUser> {
124        let users = self.users.read().await;
125        let user = users
126            .get(username)
127            .ok_or_else(|| Error::generic("User not found".to_string()))?;
128
129        // Verify password
130        if let Some(ref hash) = user.password_hash {
131            if !self.verify_password(password, hash) {
132                return Err(Error::generic("Invalid password".to_string()));
133            }
134        }
135
136        Ok(user.clone())
137    }
138
139    /// Generate JWT token for a user
140    ///
141    /// Automatically uses virtual clock if time travel is enabled,
142    /// otherwise uses real time.
143    pub fn generate_token(&self, user: &VirtualUser) -> Result<String> {
144        let now = time_travel_now();
145        let exp = now
146            .checked_add_signed(Duration::seconds(self.token_expiration as i64))
147            .ok_or_else(|| Error::generic("Invalid expiration time".to_string()))?
148            .timestamp() as usize;
149
150        let claims = JwtClaims {
151            sub: user.id.to_string(),
152            username: user.username.clone(),
153            email: user.email.clone(),
154            exp,
155            iat: now.timestamp() as usize,
156            roles: user.roles.clone(),
157        };
158
159        // Use jsonwebtoken crate if available, otherwise return error
160        #[cfg(feature = "jwt")]
161        {
162            use jsonwebtoken::{encode, EncodingKey, Header};
163            let token = encode(
164                &Header::default(),
165                &claims,
166                &EncodingKey::from_secret(self.jwt_secret.as_bytes()),
167            )
168            .map_err(|e| Error::generic(format!("Token generation failed: {}", e)))?;
169            Ok(token)
170        }
171
172        #[cfg(not(feature = "jwt"))]
173        {
174            // Fallback: return a simple token format (not secure, for testing only)
175            let token_data = serde_json::to_string(&claims)
176                .map_err(|e| Error::generic(format!("Serialization failed: {}", e)))?;
177            Ok(format!("vbr.{}", base64::encode(&token_data)))
178        }
179    }
180
181    /// Validate JWT token
182    pub fn validate_token(&self, token: &str) -> Result<VirtualUser> {
183        #[cfg(feature = "jwt")]
184        {
185            use jsonwebtoken::{decode, DecodingKey, Validation};
186            let validation = Validation::default();
187            let token_data = decode::<JwtClaims>(
188                token,
189                &DecodingKey::from_secret(self.jwt_secret.as_bytes()),
190                &validation,
191            )
192            .map_err(|e| Error::generic(format!("Token validation failed: {}", e)))?;
193
194            if token_data.claims.is_expired() {
195                return Err(Error::generic("Token expired".to_string()));
196            }
197
198            Ok(VirtualUser {
199                id: Uuid::parse_str(&token_data.claims.sub)
200                    .map_err(|e| Error::generic(format!("Invalid user ID: {}", e)))?,
201                username: token_data.claims.username,
202                email: token_data.claims.email,
203                password_hash: None,
204                roles: token_data.claims.roles,
205            })
206        }
207
208        #[cfg(not(feature = "jwt"))]
209        {
210            // Fallback: decode simple token format
211            if let Some(token_data) = token.strip_prefix("vbr.") {
212                let decoded = base64::decode(token_data)
213                    .map_err(|e| Error::generic(format!("Token decode failed: {}", e)))?;
214                let claims: JwtClaims = serde_json::from_slice(&decoded)
215                    .map_err(|e| Error::generic(format!("Token parse failed: {}", e)))?;
216
217                if claims.is_expired() {
218                    return Err(Error::generic("Token expired".to_string()));
219                }
220
221                Ok(VirtualUser {
222                    id: Uuid::parse_str(&claims.sub)
223                        .map_err(|e| Error::generic(format!("Invalid user ID: {}", e)))?,
224                    username: claims.username,
225                    email: claims.email,
226                    password_hash: None,
227                    roles: claims.roles,
228                })
229            } else {
230                Err(Error::generic("Invalid token format".to_string()))
231            }
232        }
233    }
234
235    /// Get user by username
236    pub async fn get_user(&self, username: &str) -> Option<VirtualUser> {
237        let users = self.users.read().await;
238        users.get(username).cloned()
239    }
240
241    /// List all users
242    pub async fn list_users(&self) -> Vec<VirtualUser> {
243        let users = self.users.read().await;
244        users.values().cloned().collect()
245    }
246}
247
248// Add base64 encoding for fallback implementation
249#[cfg(not(feature = "jwt"))]
250mod base64 {
251    use base64::{engine::general_purpose, Engine as _};
252    pub fn encode(data: &str) -> String {
253        general_purpose::STANDARD.encode(data.as_bytes())
254    }
255    pub fn decode(data: &str) -> Result<Vec<u8>, String> {
256        general_purpose::STANDARD
257            .decode(data)
258            .map_err(|e| format!("Decode error: {}", e))
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    // VirtualUser tests
267    #[test]
268    fn test_virtual_user_clone() {
269        let user = VirtualUser {
270            id: Uuid::new_v4(),
271            username: "testuser".to_string(),
272            email: "test@example.com".to_string(),
273            password_hash: Some("hash123".to_string()),
274            roles: vec!["admin".to_string()],
275        };
276
277        let cloned = user.clone();
278        assert_eq!(user.username, cloned.username);
279        assert_eq!(user.email, cloned.email);
280        assert_eq!(user.roles, cloned.roles);
281    }
282
283    #[test]
284    fn test_virtual_user_debug() {
285        let user = VirtualUser {
286            id: Uuid::new_v4(),
287            username: "debuguser".to_string(),
288            email: "debug@test.com".to_string(),
289            password_hash: None,
290            roles: Vec::new(),
291        };
292
293        let debug = format!("{:?}", user);
294        assert!(debug.contains("VirtualUser"));
295        assert!(debug.contains("debuguser"));
296    }
297
298    #[test]
299    fn test_virtual_user_serialize() {
300        let user = VirtualUser {
301            id: Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
302            username: "serialuser".to_string(),
303            email: "serial@test.com".to_string(),
304            password_hash: Some("secret".to_string()),
305            roles: vec!["user".to_string()],
306        };
307
308        let json = serde_json::to_string(&user).unwrap();
309        assert!(json.contains("serialuser"));
310        assert!(json.contains("serial@test.com"));
311        // password_hash should be skipped during serialization
312        assert!(!json.contains("secret"));
313    }
314
315    #[test]
316    fn test_virtual_user_deserialize() {
317        let json = r#"{
318            "id": "550e8400-e29b-41d4-a716-446655440000",
319            "username": "deserialuser",
320            "email": "deserial@test.com",
321            "roles": ["admin", "user"]
322        }"#;
323
324        let user: VirtualUser = serde_json::from_str(json).unwrap();
325        assert_eq!(user.username, "deserialuser");
326        assert_eq!(user.email, "deserial@test.com");
327        assert_eq!(user.roles, vec!["admin", "user"]);
328    }
329
330    #[test]
331    fn test_virtual_user_default_roles() {
332        let json = r#"{
333            "id": "550e8400-e29b-41d4-a716-446655440000",
334            "username": "norolesuser",
335            "email": "noroles@test.com"
336        }"#;
337
338        let user: VirtualUser = serde_json::from_str(json).unwrap();
339        assert!(user.roles.is_empty());
340    }
341
342    #[test]
343    fn test_virtual_user_with_multiple_roles() {
344        let user = VirtualUser {
345            id: Uuid::new_v4(),
346            username: "multirole".to_string(),
347            email: "multi@test.com".to_string(),
348            password_hash: None,
349            roles: vec![
350                "admin".to_string(),
351                "moderator".to_string(),
352                "user".to_string(),
353            ],
354        };
355
356        assert_eq!(user.roles.len(), 3);
357        assert!(user.roles.contains(&"admin".to_string()));
358        assert!(user.roles.contains(&"moderator".to_string()));
359    }
360
361    // VbrAuthService tests
362    #[test]
363    fn test_vbr_auth_service_new() {
364        let service = VbrAuthService::new("test_secret".to_string(), 3600);
365        assert_eq!(service.jwt_secret, "test_secret");
366        assert_eq!(service.token_expiration, 3600);
367    }
368
369    #[test]
370    fn test_vbr_auth_service_hash_password() {
371        let service = VbrAuthService::new("secret".to_string(), 3600);
372        let hash1 = service.hash_password("password123").unwrap();
373        let hash2 = service.hash_password("password123").unwrap();
374
375        // Same password should produce same hash
376        assert_eq!(hash1, hash2);
377
378        // Different passwords should produce different hashes
379        let hash3 = service.hash_password("different").unwrap();
380        assert_ne!(hash1, hash3);
381    }
382
383    #[test]
384    fn test_vbr_auth_service_verify_password() {
385        let service = VbrAuthService::new("secret".to_string(), 3600);
386        let hash = service.hash_password("mypassword").unwrap();
387
388        assert!(service.verify_password("mypassword", &hash));
389        assert!(!service.verify_password("wrongpassword", &hash));
390    }
391
392    #[tokio::test]
393    async fn test_vbr_auth_service_create_default_user() {
394        let service = VbrAuthService::new("secret".to_string(), 3600);
395
396        let user = service
397            .create_default_user(
398                "newuser".to_string(),
399                "password123".to_string(),
400                "new@test.com".to_string(),
401            )
402            .await
403            .unwrap();
404
405        assert_eq!(user.username, "newuser");
406        assert_eq!(user.email, "new@test.com");
407        assert!(user.password_hash.is_some());
408    }
409
410    #[tokio::test]
411    async fn test_vbr_auth_service_authenticate_success() {
412        let service = VbrAuthService::new("secret".to_string(), 3600);
413
414        service
415            .create_default_user(
416                "authuser".to_string(),
417                "correctpass".to_string(),
418                "auth@test.com".to_string(),
419            )
420            .await
421            .unwrap();
422
423        let result = service.authenticate("authuser", "correctpass").await;
424        assert!(result.is_ok());
425        let user = result.unwrap();
426        assert_eq!(user.username, "authuser");
427    }
428
429    #[tokio::test]
430    async fn test_vbr_auth_service_authenticate_wrong_password() {
431        let service = VbrAuthService::new("secret".to_string(), 3600);
432
433        service
434            .create_default_user(
435                "wrongpassuser".to_string(),
436                "correctpass".to_string(),
437                "wrong@test.com".to_string(),
438            )
439            .await
440            .unwrap();
441
442        let result = service.authenticate("wrongpassuser", "wrongpass").await;
443        assert!(result.is_err());
444    }
445
446    #[tokio::test]
447    async fn test_vbr_auth_service_authenticate_user_not_found() {
448        let service = VbrAuthService::new("secret".to_string(), 3600);
449
450        let result = service.authenticate("nonexistent", "anypass").await;
451        assert!(result.is_err());
452    }
453
454    #[tokio::test]
455    async fn test_vbr_auth_service_get_user() {
456        let service = VbrAuthService::new("secret".to_string(), 3600);
457
458        service
459            .create_default_user(
460                "getuser".to_string(),
461                "pass".to_string(),
462                "get@test.com".to_string(),
463            )
464            .await
465            .unwrap();
466
467        let user = service.get_user("getuser").await;
468        assert!(user.is_some());
469        assert_eq!(user.unwrap().email, "get@test.com");
470
471        let nonexistent = service.get_user("nonexistent").await;
472        assert!(nonexistent.is_none());
473    }
474
475    #[tokio::test]
476    async fn test_vbr_auth_service_list_users() {
477        let service = VbrAuthService::new("secret".to_string(), 3600);
478
479        // Initially empty
480        let users = service.list_users().await;
481        assert!(users.is_empty());
482
483        // Add users
484        service
485            .create_default_user(
486                "user1".to_string(),
487                "pass1".to_string(),
488                "u1@test.com".to_string(),
489            )
490            .await
491            .unwrap();
492
493        service
494            .create_default_user(
495                "user2".to_string(),
496                "pass2".to_string(),
497                "u2@test.com".to_string(),
498            )
499            .await
500            .unwrap();
501
502        let users = service.list_users().await;
503        assert_eq!(users.len(), 2);
504    }
505
506    #[tokio::test]
507    async fn test_vbr_auth_service_generate_token() {
508        let service = VbrAuthService::new("my_jwt_secret".to_string(), 3600);
509
510        let user = service
511            .create_default_user(
512                "tokenuser".to_string(),
513                "pass".to_string(),
514                "token@test.com".to_string(),
515            )
516            .await
517            .unwrap();
518
519        let token = service.generate_token(&user);
520        assert!(token.is_ok());
521        let token_str = token.unwrap();
522        assert!(!token_str.is_empty());
523    }
524
525    #[tokio::test]
526    async fn test_vbr_auth_service_validate_token() {
527        let service = VbrAuthService::new("jwt_secret_for_test".to_string(), 3600);
528
529        let user = service
530            .create_default_user(
531                "validateuser".to_string(),
532                "pass".to_string(),
533                "validate@test.com".to_string(),
534            )
535            .await
536            .unwrap();
537
538        let token = service.generate_token(&user).unwrap();
539        let validated_user = service.validate_token(&token);
540
541        assert!(validated_user.is_ok());
542        let validated = validated_user.unwrap();
543        assert_eq!(validated.username, "validateuser");
544        assert_eq!(validated.email, "validate@test.com");
545    }
546
547    #[test]
548    fn test_vbr_auth_service_validate_invalid_token() {
549        let service = VbrAuthService::new("secret".to_string(), 3600);
550
551        let result = service.validate_token("invalid_token");
552        assert!(result.is_err());
553    }
554
555    #[test]
556    fn test_vbr_auth_service_validate_malformed_token() {
557        let service = VbrAuthService::new("secret".to_string(), 3600);
558
559        // Token without vbr. prefix (for non-jwt feature)
560        let result = service.validate_token("not.vbr.token");
561        assert!(result.is_err());
562    }
563
564    #[test]
565    fn test_hash_different_secrets_produce_different_hashes() {
566        let service1 = VbrAuthService::new("secret1".to_string(), 3600);
567        let service2 = VbrAuthService::new("secret2".to_string(), 3600);
568
569        let hash1 = service1.hash_password("samepassword").unwrap();
570        let hash2 = service2.hash_password("samepassword").unwrap();
571
572        // Same password with different salts (jwt_secret) should produce different hashes
573        assert_ne!(hash1, hash2);
574    }
575
576    #[tokio::test]
577    async fn test_vbr_auth_service_user_with_roles() {
578        let service = VbrAuthService::new("secret".to_string(), 3600);
579
580        let user = service
581            .create_default_user(
582                "roleuser".to_string(),
583                "pass".to_string(),
584                "role@test.com".to_string(),
585            )
586            .await
587            .unwrap();
588
589        // Default user has no roles
590        assert!(user.roles.is_empty());
591    }
592}