Skip to main content

mockforge_ui/
auth.rs

1//! Authentication and JWT token management
2//!
3//! This module provides authentication endpoints and JWT token generation/validation
4//! for the Admin UI.
5//!
6//! # Features
7//! - JWT token generation and validation
8//! - Password hashing with bcrypt
9//! - Rate limiting for login attempts
10//! - In-memory user store (can be replaced with database)
11//!
12//! # Database Integration
13//! See `auth/database.rs` for database-backed user store implementation.
14
15use axum::{extract::State, http::StatusCode, response::Json};
16use bcrypt::{hash, verify, DEFAULT_COST};
17use chrono::{Duration, Utc};
18use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::sync::Arc;
22use std::time::{Duration as StdDuration, Instant};
23use tokio::sync::RwLock;
24
25use crate::handlers::AdminState;
26use crate::models::ApiResponse;
27use crate::rbac::UserContext;
28use mockforge_collab::models::UserRole;
29
30mod password_policy;
31pub use password_policy::{PasswordPolicy, PasswordValidationError};
32
33const MIN_JWT_SECRET_LEN: usize = 32;
34
35fn is_truthy_env(name: &str) -> bool {
36    matches!(
37        std::env::var(name).ok().as_deref().map(str::to_ascii_lowercase).as_deref(),
38        Some("1") | Some("true") | Some("yes") | Some("on")
39    )
40}
41
42fn is_development_environment() -> bool {
43    if cfg!(test) {
44        return true;
45    }
46
47    matches!(
48        std::env::var("ENVIRONMENT")
49            .unwrap_or_else(|_| "production".to_string())
50            .to_ascii_lowercase()
51            .as_str(),
52        "development" | "dev" | "local"
53    )
54}
55
56fn is_dev_auth_enabled() -> bool {
57    cfg!(test) || is_truthy_env("MOCKFORGE_ENABLE_DEV_AUTH")
58}
59
60fn should_seed_default_users() -> bool {
61    // Seed users in dev environment with dev auth enabled, OR when no production auth is configured
62    // (so `--admin` works out of the box with default credentials)
63    let explicit_dev = is_development_environment() && is_dev_auth_enabled();
64    let no_production_auth = !is_development_environment() && std::env::var("JWT_SECRET").is_err();
65    (explicit_dev || no_production_auth) && !is_truthy_env("MOCKFORGE_DISABLE_DEV_SEED_USERS")
66}
67
68fn get_jwt_secret_bytes() -> Result<Vec<u8>, jsonwebtoken::errors::Error> {
69    if cfg!(test) {
70        return Ok(b"test-jwt-secret-which-is-long-enough".to_vec());
71    }
72
73    if let Ok(secret) = std::env::var("JWT_SECRET") {
74        if secret.len() < MIN_JWT_SECRET_LEN {
75            tracing::error!(
76                "JWT_SECRET is too short ({} chars). Minimum required is {} chars.",
77                secret.len(),
78                MIN_JWT_SECRET_LEN
79            );
80            return Err(jsonwebtoken::errors::Error::from(
81                jsonwebtoken::errors::ErrorKind::InvalidToken,
82            ));
83        }
84        return Ok(secret.into_bytes());
85    }
86
87    if is_development_environment() && is_dev_auth_enabled() {
88        let dev_secret = std::env::var("MOCKFORGE_DEV_JWT_SECRET")
89            .unwrap_or_else(|_| "mockforge-dev-only-secret-change-me-12345".to_string());
90        tracing::warn!(
91            "Using development JWT secret fallback. Set JWT_SECRET for production-like testing."
92        );
93        return Ok(dev_secret.into_bytes());
94    }
95
96    // Fallback: auto-generate a secret so `--admin` works out of the box
97    tracing::warn!(
98        "Using auto-generated JWT secret for in-memory auth. Set JWT_SECRET for production use."
99    );
100    Ok("mockforge-auto-jwt-secret-for-admin-ui-1234".as_bytes().to_vec())
101}
102
103pub fn validate_auth_config_on_startup() -> Result<(), String> {
104    if !is_development_environment() && !is_truthy_env("MOCKFORGE_ALLOW_INMEMORY_AUTH") {
105        // Auto-allow in-memory auth for mock server usage, but warn
106        tracing::warn!(
107            "No production auth backend configured. Using in-memory auth. \
108             Set ENVIRONMENT=production and configure a real auth backend for production use, \
109             or suppress this warning with MOCKFORGE_ALLOW_INMEMORY_AUTH=true"
110        );
111    }
112
113    get_jwt_secret_bytes()
114        .map(|_| ())
115        .map_err(|_| "JWT_SECRET is missing or invalid for current environment".to_string())
116}
117
118/// JWT claims structure
119#[derive(Debug, Serialize, Deserialize)]
120pub struct Claims {
121    /// Subject (user ID)
122    pub sub: String,
123    /// Username
124    pub username: String,
125    /// User role
126    pub role: String,
127    /// Email (optional)
128    pub email: Option<String>,
129    /// Issued at
130    pub iat: i64,
131    /// Expiration time
132    pub exp: i64,
133}
134
135/// Login request
136#[derive(Debug, Deserialize)]
137pub struct LoginRequest {
138    pub username: String,
139    pub password: String,
140}
141
142/// Login response
143#[derive(Debug, Serialize)]
144pub struct LoginResponse {
145    pub token: String,
146    pub refresh_token: String,
147    pub user: UserInfo,
148    pub expires_in: i64,
149}
150
151/// User information
152#[derive(Debug, Serialize, Clone)]
153pub struct UserInfo {
154    pub id: String,
155    pub username: String,
156    pub role: String,
157    pub email: Option<String>,
158}
159
160/// Refresh token request
161#[derive(Debug, Deserialize)]
162pub struct RefreshTokenRequest {
163    pub refresh_token: String,
164}
165
166/// In-memory user store (in production, use database)
167#[derive(Debug, Clone)]
168pub struct UserStore {
169    users: Arc<RwLock<HashMap<String, User>>>,
170    rate_limiter: RateLimiter,
171    account_lockout: AccountLockout,
172    #[allow(dead_code)]
173    password_policy: PasswordPolicy,
174}
175
176#[derive(Debug, Clone)]
177pub(crate) struct User {
178    id: String,
179    username: String,
180    password_hash: String, // Bcrypt hashed password
181    role: UserRole,
182    email: Option<String>,
183}
184
185/// Rate limiting for login attempts
186#[derive(Debug, Clone)]
187struct RateLimiter {
188    attempts: Arc<RwLock<HashMap<String, Vec<Instant>>>>,
189    max_attempts: usize,
190    window_seconds: u64,
191}
192
193/// Account lockout configuration
194#[derive(Debug, Clone)]
195struct AccountLockout {
196    /// Failed login attempts per account
197    #[allow(clippy::type_complexity)]
198    failed_attempts: Arc<RwLock<HashMap<String, (usize, Option<Instant>)>>>,
199    /// Maximum failed attempts before lockout
200    max_failed_attempts: usize,
201    /// Lockout duration in seconds
202    lockout_duration_seconds: u64,
203}
204
205impl AccountLockout {
206    fn new(max_failed_attempts: usize, lockout_duration_seconds: u64) -> Self {
207        Self {
208            failed_attempts: Arc::new(RwLock::new(HashMap::new())),
209            max_failed_attempts,
210            lockout_duration_seconds,
211        }
212    }
213
214    /// Check if account is locked
215    async fn is_locked(&self, username: &str) -> bool {
216        let attempts = self.failed_attempts.read().await;
217        if let Some((count, locked_until)) = attempts.get(username) {
218            if *count >= self.max_failed_attempts {
219                if let Some(until) = locked_until {
220                    return until > &Instant::now();
221                }
222            }
223        }
224        false
225    }
226
227    /// Record a failed login attempt
228    async fn record_failure(&self, username: &str) {
229        let mut attempts = self.failed_attempts.write().await;
230        let entry = attempts.entry(username.to_string()).or_insert((0, None));
231        entry.0 += 1;
232
233        if entry.0 >= self.max_failed_attempts {
234            entry.1 = Some(Instant::now() + StdDuration::from_secs(self.lockout_duration_seconds));
235            tracing::warn!("Account locked: {} ({} failed attempts)", username, entry.0);
236        }
237    }
238
239    /// Reset failed attempts on successful login
240    async fn reset(&self, username: &str) {
241        let mut attempts = self.failed_attempts.write().await;
242        attempts.remove(username);
243    }
244
245    /// Get remaining lockout time in seconds
246    #[allow(clippy::collapsible_match)]
247    async fn remaining_lockout_time(&self, username: &str) -> Option<u64> {
248        let attempts = self.failed_attempts.read().await;
249        if let Some((_, locked_until)) = attempts.get(username) {
250            if let Some(until) = locked_until {
251                let now = Instant::now();
252                if until > &now {
253                    return Some(until.duration_since(now).as_secs());
254                }
255            }
256        }
257        None
258    }
259}
260
261impl RateLimiter {
262    fn new(max_attempts: usize, window_seconds: u64) -> Self {
263        Self {
264            attempts: Arc::new(RwLock::new(HashMap::new())),
265            max_attempts,
266            window_seconds,
267        }
268    }
269
270    async fn check_rate_limit(&self, key: &str) -> Result<(), String> {
271        let mut attempts = self.attempts.write().await;
272        let now = Instant::now();
273        let window = StdDuration::from_secs(self.window_seconds);
274
275        // Clean up old attempts
276        if let Some(attempt_times) = attempts.get_mut(key) {
277            attempt_times.retain(|&time| now.duration_since(time) < window);
278
279            if attempt_times.len() >= self.max_attempts {
280                return Err(format!(
281                    "Too many login attempts. Please try again in {} seconds.",
282                    self.window_seconds
283                ));
284            }
285        }
286
287        // Record this attempt
288        attempts.entry(key.to_string()).or_insert_with(Vec::new).push(now);
289
290        Ok(())
291    }
292
293    async fn reset_rate_limit(&self, key: &str) {
294        let mut attempts = self.attempts.write().await;
295        attempts.remove(key);
296    }
297}
298
299impl Default for UserStore {
300    fn default() -> Self {
301        Self::new()
302    }
303}
304
305impl UserStore {
306    pub fn new() -> Self {
307        let users = Arc::new(RwLock::new(HashMap::new()));
308        let rate_limiter = RateLimiter::new(5, 300); // 5 attempts per 5 minutes
309        let account_lockout = AccountLockout::new(5, 900); // 5 attempts, 15 minute lockout
310        let password_policy = PasswordPolicy::default(); // Use default policy
311
312        let store = Self {
313            users,
314            rate_limiter,
315            account_lockout,
316            password_policy,
317        };
318
319        if should_seed_default_users() {
320            // Development-only seeded users for local testing
321            let default_users = vec![
322                ("admin", "admin123", UserRole::Admin, "admin@mockforge.dev"),
323                ("viewer", "viewer123", UserRole::Viewer, "viewer@mockforge.dev"),
324                ("editor", "editor123", UserRole::Editor, "editor@mockforge.dev"),
325            ];
326
327            let store_clone = store.clone();
328            tokio::spawn(async move {
329                let mut users = store_clone.users.write().await;
330                for (username, password, role, email) in default_users {
331                    if let Ok(password_hash) = hash(password, DEFAULT_COST) {
332                        let user = User {
333                            id: format!("{}-001", username),
334                            username: username.to_string(),
335                            password_hash,
336                            role,
337                            email: Some(email.to_string()),
338                        };
339                        users.insert(username.to_string(), user);
340                    } else {
341                        tracing::error!("Failed to hash password for user: {}", username);
342                    }
343                }
344            });
345        }
346
347        store
348    }
349
350    pub(crate) async fn authenticate(
351        &self,
352        username: &str,
353        password: &str,
354    ) -> Result<User, String> {
355        // Check if account is locked
356        if self.account_lockout.is_locked(username).await {
357            if let Some(remaining) = self.account_lockout.remaining_lockout_time(username).await {
358                return Err(format!(
359                    "Account is locked due to too many failed login attempts. Please try again in {} seconds.",
360                    remaining
361                ));
362            }
363        }
364
365        // Check rate limiting
366        self.rate_limiter.check_rate_limit(username).await?;
367
368        let users = self.users.read().await;
369        if let Some(user) = users.get(username) {
370            // Verify password with bcrypt
371            match verify(password, &user.password_hash) {
372                Ok(true) => {
373                    // Successful login - reset rate limit and lockout
374                    self.rate_limiter.reset_rate_limit(username).await;
375                    self.account_lockout.reset(username).await;
376                    Ok(user.clone())
377                }
378                Ok(false) => {
379                    // Wrong password - record failure
380                    self.account_lockout.record_failure(username).await;
381                    Err("Invalid username or password".to_string())
382                }
383                Err(e) => {
384                    tracing::error!("Password verification error: {}", e);
385                    Err("Authentication error".to_string())
386                }
387            }
388        } else {
389            // User not found - still count as failed attempt (but don't lock non-existent accounts)
390            Err("Invalid username or password".to_string())
391        }
392    }
393
394    /// Create a new user with password policy validation
395    #[allow(dead_code)]
396    pub(crate) async fn create_user(
397        &self,
398        username: String,
399        password: String,
400        role: UserRole,
401        email: Option<String>,
402    ) -> Result<User, String> {
403        // Validate password against policy
404        #[cfg(feature = "password-policy")]
405        {
406            self.password_policy
407                .validate(&password, Some(&username))
408                .map_err(|e| e.to_string())?;
409        }
410
411        // Check if user already exists
412        let mut users = self.users.write().await;
413        if users.contains_key(&username) {
414            return Err("Username already exists".to_string());
415        }
416
417        // Hash password
418        let password_hash =
419            hash(&password, DEFAULT_COST).map_err(|e| format!("Failed to hash password: {}", e))?;
420
421        // Create user
422        let user = User {
423            id: format!("{}-{}", username, uuid::Uuid::new_v4()),
424            username: username.clone(),
425            password_hash,
426            role,
427            email,
428        };
429
430        users.insert(username, user.clone());
431        Ok(user)
432    }
433
434    pub(crate) async fn get_user_by_id(&self, user_id: &str) -> Option<User> {
435        let users = self.users.read().await;
436        users.values().find(|u| u.id == user_id).cloned()
437    }
438}
439
440/// Global user store instance
441static GLOBAL_USER_STORE: std::sync::OnceLock<Arc<UserStore>> = std::sync::OnceLock::new();
442
443/// Global set of revoked JWT tokens (jti claim or raw token string)
444/// Tokens are stored with their expiry time so we can prune expired entries.
445static REVOKED_TOKENS: std::sync::OnceLock<Arc<RwLock<HashMap<String, i64>>>> =
446    std::sync::OnceLock::new();
447
448/// Get or initialize the revoked tokens store
449fn get_revoked_tokens() -> Arc<RwLock<HashMap<String, i64>>> {
450    REVOKED_TOKENS.get_or_init(|| Arc::new(RwLock::new(HashMap::new()))).clone()
451}
452
453/// Revoke a token so it can no longer be used
454pub async fn revoke_token(token: &str) {
455    // Decode without validation to get expiry (we want to revoke even expired tokens)
456    let exp = validate_token(token)
457        .map(|c| c.exp)
458        .unwrap_or_else(|_| Utc::now().timestamp() + 7 * 24 * 60 * 60);
459    let store = get_revoked_tokens();
460    let mut revoked = store.write().await;
461    revoked.insert(token.to_string(), exp);
462    // Prune expired entries to prevent unbounded growth
463    let now = Utc::now().timestamp();
464    revoked.retain(|_, &mut exp_time| exp_time > now);
465}
466
467/// Check if a token has been revoked
468pub async fn is_token_revoked(token: &str) -> bool {
469    let store = get_revoked_tokens();
470    let revoked = store.read().await;
471    revoked.contains_key(token)
472}
473
474/// Initialize the global user store
475pub fn init_global_user_store() -> Arc<UserStore> {
476    if let Err(e) = validate_auth_config_on_startup() {
477        panic!("Authentication startup validation failed: {}", e);
478    }
479    GLOBAL_USER_STORE.get_or_init(|| Arc::new(UserStore::new())).clone()
480}
481
482/// Get the global user store
483pub fn get_global_user_store() -> Option<Arc<UserStore>> {
484    GLOBAL_USER_STORE.get().cloned()
485}
486
487/// Generate JWT token
488pub(crate) fn generate_token(
489    user: &User,
490    expires_in_seconds: i64,
491) -> Result<String, jsonwebtoken::errors::Error> {
492    let now = Utc::now();
493    let exp = now + Duration::seconds(expires_in_seconds);
494    let secret = get_jwt_secret_bytes()?;
495
496    let claims = Claims {
497        sub: user.id.clone(),
498        username: user.username.clone(),
499        role: format!("{:?}", user.role).to_lowercase(),
500        email: user.email.clone(),
501        iat: now.timestamp(),
502        exp: exp.timestamp(),
503    };
504
505    encode(&Header::default(), &claims, &EncodingKey::from_secret(&secret))
506}
507
508/// Generate refresh token
509pub(crate) fn generate_refresh_token(user: &User) -> Result<String, jsonwebtoken::errors::Error> {
510    // Refresh tokens expire in 7 days
511    generate_token(user, 7 * 24 * 60 * 60)
512}
513
514/// Validate JWT token
515pub fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
516    let secret = get_jwt_secret_bytes()?;
517    let token_data =
518        decode::<Claims>(token, &DecodingKey::from_secret(&secret), &Validation::default())?;
519
520    Ok(token_data.claims)
521}
522
523/// Login endpoint
524pub async fn login(
525    State(_state): State<AdminState>,
526    Json(request): Json<LoginRequest>,
527) -> Result<Json<ApiResponse<LoginResponse>>, StatusCode> {
528    let user_store = get_global_user_store().ok_or_else(|| {
529        tracing::error!("User store not initialized");
530        StatusCode::INTERNAL_SERVER_ERROR
531    })?;
532
533    // Authenticate user
534    let user =
535        user_store
536            .authenticate(&request.username, &request.password)
537            .await
538            .map_err(|e| {
539                tracing::warn!("Authentication failed for user {}: {}", request.username, e);
540                // Return appropriate status code based on error
541                if e.contains("Too many") {
542                    StatusCode::TOO_MANY_REQUESTS
543                } else {
544                    StatusCode::UNAUTHORIZED
545                }
546            })?;
547
548    // Generate tokens
549    let access_token = generate_token(&user, 24 * 60 * 60) // 24 hours
550        .map_err(|e| {
551            tracing::error!("Failed to generate access token: {}", e);
552            StatusCode::INTERNAL_SERVER_ERROR
553        })?;
554
555    let refresh_token = generate_refresh_token(&user).map_err(|e| {
556        tracing::error!("Failed to generate refresh token: {}", e);
557        StatusCode::INTERNAL_SERVER_ERROR
558    })?;
559
560    let user_info = UserInfo {
561        id: user.id,
562        username: user.username,
563        role: format!("{:?}", user.role).to_lowercase(),
564        email: user.email,
565    };
566
567    Ok(Json(ApiResponse::success(LoginResponse {
568        token: access_token,
569        refresh_token,
570        user: user_info,
571        expires_in: 24 * 60 * 60,
572    })))
573}
574
575/// Refresh token endpoint
576pub async fn refresh_token(
577    State(_state): State<AdminState>,
578    Json(request): Json<RefreshTokenRequest>,
579) -> Result<Json<ApiResponse<LoginResponse>>, StatusCode> {
580    // Check if the refresh token has been revoked
581    if is_token_revoked(&request.refresh_token).await {
582        tracing::warn!("Attempt to use revoked refresh token");
583        return Err(StatusCode::UNAUTHORIZED);
584    }
585
586    // Validate refresh token
587    let claims = validate_token(&request.refresh_token).map_err(|_| {
588        tracing::warn!("Invalid refresh token");
589        StatusCode::UNAUTHORIZED
590    })?;
591
592    let user_store = get_global_user_store().ok_or_else(|| {
593        tracing::error!("User store not initialized");
594        StatusCode::INTERNAL_SERVER_ERROR
595    })?;
596
597    // Get user
598    let user = user_store.get_user_by_id(&claims.sub).await.ok_or_else(|| {
599        tracing::warn!("User not found: {}", claims.sub);
600        StatusCode::UNAUTHORIZED
601    })?;
602
603    // Generate new tokens
604    let access_token = generate_token(&user, 24 * 60 * 60) // 24 hours
605        .map_err(|e| {
606            tracing::error!("Failed to generate access token: {}", e);
607            StatusCode::INTERNAL_SERVER_ERROR
608        })?;
609
610    let refresh_token = generate_refresh_token(&user).map_err(|e| {
611        tracing::error!("Failed to generate refresh token: {}", e);
612        StatusCode::INTERNAL_SERVER_ERROR
613    })?;
614
615    let user_info = UserInfo {
616        id: user.id,
617        username: user.username,
618        role: format!("{:?}", user.role).to_lowercase(),
619        email: user.email,
620    };
621
622    Ok(Json(ApiResponse::success(LoginResponse {
623        token: access_token,
624        refresh_token,
625        user: user_info,
626        expires_in: 24 * 60 * 60,
627    })))
628}
629
630/// Get current user endpoint
631pub async fn get_current_user(
632    headers: axum::http::HeaderMap,
633) -> Result<Json<ApiResponse<UserInfo>>, StatusCode> {
634    let auth_header = headers
635        .get("authorization")
636        .and_then(|h| h.to_str().ok())
637        .ok_or(StatusCode::UNAUTHORIZED)?;
638
639    let token = auth_header.strip_prefix("Bearer ").ok_or(StatusCode::UNAUTHORIZED)?;
640
641    let claims = validate_token(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
642    let role = claims_to_user_context(&claims).role;
643
644    Ok(Json(ApiResponse::success(UserInfo {
645        id: claims.sub,
646        username: claims.username,
647        role: format!("{:?}", role).to_lowercase(),
648        email: claims.email,
649    })))
650}
651
652/// Logout endpoint — revokes the bearer token server-side
653pub async fn logout(
654    headers: axum::http::HeaderMap,
655    State(_state): State<AdminState>,
656) -> Json<ApiResponse<String>> {
657    // Extract and revoke the access token from the Authorization header
658    if let Some(auth_header) = headers.get("authorization").and_then(|h| h.to_str().ok()) {
659        if let Some(token) = auth_header.strip_prefix("Bearer ") {
660            revoke_token(token).await;
661            tracing::info!("Token revoked on logout");
662        }
663    }
664    Json(ApiResponse::success("Logged out successfully".to_string()))
665}
666
667/// Convert Claims to UserContext
668pub fn claims_to_user_context(claims: &Claims) -> UserContext {
669    let role = match claims.role.as_str() {
670        "admin" => UserRole::Admin,
671        "editor" => UserRole::Editor,
672        "viewer" => UserRole::Viewer,
673        _ => UserRole::Viewer,
674    };
675
676    UserContext {
677        user_id: claims.sub.clone(),
678        username: claims.username.clone(),
679        role,
680        email: claims.email.clone(),
681    }
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687
688    #[tokio::test]
689    async fn test_user_store_creation() {
690        let store = UserStore::new();
691        // Wait a bit for async initialization
692        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
693
694        // Verify default users exist
695        let result = store.authenticate("admin", "admin123").await;
696        assert!(result.is_ok(), "Admin user should exist");
697    }
698
699    #[tokio::test]
700    async fn test_user_store_default() {
701        let store = UserStore::default();
702        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
703
704        let result = store.authenticate("admin", "admin123").await;
705        assert!(result.is_ok());
706    }
707
708    #[tokio::test]
709    async fn test_authenticate_success() {
710        let store = UserStore::new();
711        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
712
713        let result = store.authenticate("admin", "admin123").await;
714        assert!(result.is_ok());
715
716        let user = result.unwrap();
717        assert_eq!(user.username, "admin");
718        assert!(matches!(user.role, UserRole::Admin));
719    }
720
721    #[tokio::test]
722    async fn test_authenticate_wrong_password() {
723        let store = UserStore::new();
724        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
725
726        let result = store.authenticate("admin", "wrongpassword").await;
727        assert!(result.is_err());
728        assert_eq!(result.unwrap_err(), "Invalid username or password");
729    }
730
731    #[tokio::test]
732    async fn test_authenticate_nonexistent_user() {
733        let store = UserStore::new();
734        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
735
736        let result = store.authenticate("nonexistent", "password").await;
737        assert!(result.is_err());
738        assert_eq!(result.unwrap_err(), "Invalid username or password");
739    }
740
741    #[tokio::test]
742    async fn test_rate_limiting() {
743        let store = UserStore::new();
744        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
745
746        // Try to login with wrong password many times
747        for _ in 0..5 {
748            let _ = store.authenticate("admin", "wrongpassword").await;
749        }
750
751        // Next attempt should be rate limited or account locked
752        let result = store.authenticate("admin", "wrongpassword").await;
753        assert!(result.is_err());
754        let error = result.unwrap_err();
755        assert!(
756            error.contains("Too many") || error.contains("locked"),
757            "Expected rate limit or lockout error, got: {}",
758            error
759        );
760    }
761
762    #[tokio::test]
763    async fn test_account_lockout_after_failures() {
764        let store = UserStore::new();
765        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
766
767        // Try to login with wrong password 5 times
768        for _ in 0..5 {
769            let _ = store.authenticate("editor", "wrongpassword").await;
770        }
771
772        // Account should now be locked
773        let result = store.authenticate("editor", "editor123").await;
774        assert!(result.is_err());
775        let error = result.unwrap_err();
776        assert!(
777            error.contains("locked") || error.contains("Too many"),
778            "Expected lockout error, got: {}",
779            error
780        );
781    }
782
783    #[tokio::test]
784    async fn test_account_lockout_reset_on_success() {
785        let store = UserStore::new();
786        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
787
788        // Try wrong password a few times
789        for _ in 0..2 {
790            let _ = store.authenticate("viewer", "wrongpassword").await;
791        }
792
793        // Successful login should reset counter
794        let result = store.authenticate("viewer", "viewer123").await;
795        assert!(result.is_ok());
796
797        // Should be able to attempt again without hitting lockout
798        for _ in 0..2 {
799            let _ = store.authenticate("viewer", "wrongpassword").await;
800        }
801
802        // Not yet locked (reset worked)
803        let result = store.authenticate("viewer", "wrongpassword").await;
804        assert!(result.is_err());
805        assert!(!result.unwrap_err().contains("locked"));
806    }
807
808    #[tokio::test]
809    async fn test_create_user_success() {
810        let store = UserStore::new();
811        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
812
813        let result = store
814            .create_user(
815                "newuser".to_string(),
816                "NewP@ssw0rd123".to_string(),
817                UserRole::Editor,
818                Some("newuser@example.com".to_string()),
819            )
820            .await;
821
822        assert!(result.is_ok());
823        let user = result.unwrap();
824        assert_eq!(user.username, "newuser");
825        assert!(matches!(user.role, UserRole::Editor));
826        assert_eq!(user.email, Some("newuser@example.com".to_string()));
827
828        // Verify we can authenticate with the new user
829        let auth_result = store.authenticate("newuser", "NewP@ssw0rd123").await;
830        assert!(auth_result.is_ok());
831    }
832
833    #[tokio::test]
834    async fn test_create_user_duplicate_username() {
835        let store = UserStore::new();
836        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
837
838        let result = store
839            .create_user("admin".to_string(), "NewP@ssw0rd123".to_string(), UserRole::Editor, None)
840            .await;
841
842        assert!(result.is_err());
843        assert_eq!(result.unwrap_err(), "Username already exists");
844    }
845
846    #[tokio::test]
847    async fn test_get_user_by_id() {
848        let store = UserStore::new();
849        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
850
851        // Authenticate to get user ID
852        let auth_result = store.authenticate("admin", "admin123").await.unwrap();
853        let user_id = auth_result.id.clone();
854
855        // Get user by ID
856        let result = store.get_user_by_id(&user_id).await;
857        assert!(result.is_some());
858
859        let user = result.unwrap();
860        assert_eq!(user.id, user_id);
861        assert_eq!(user.username, "admin");
862    }
863
864    #[tokio::test]
865    async fn test_get_user_by_id_not_found() {
866        let store = UserStore::new();
867        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
868
869        let result = store.get_user_by_id("nonexistent-id").await;
870        assert!(result.is_none());
871    }
872
873    #[test]
874    fn test_generate_token_success() {
875        let user = User {
876            id: "test-id".to_string(),
877            username: "testuser".to_string(),
878            password_hash: "hash".to_string(),
879            role: UserRole::Editor,
880            email: Some("test@example.com".to_string()),
881        };
882
883        let result = generate_token(&user, 3600);
884        assert!(result.is_ok());
885
886        let token = result.unwrap();
887        assert!(!token.is_empty());
888    }
889
890    #[test]
891    fn test_generate_refresh_token() {
892        let user = User {
893            id: "test-id".to_string(),
894            username: "testuser".to_string(),
895            password_hash: "hash".to_string(),
896            role: UserRole::Editor,
897            email: None,
898        };
899
900        let result = generate_refresh_token(&user);
901        assert!(result.is_ok());
902
903        let token = result.unwrap();
904        assert!(!token.is_empty());
905    }
906
907    #[test]
908    fn test_validate_token_success() {
909        let user = User {
910            id: "test-id".to_string(),
911            username: "testuser".to_string(),
912            password_hash: "hash".to_string(),
913            role: UserRole::Viewer,
914            email: Some("test@example.com".to_string()),
915        };
916
917        let token = generate_token(&user, 3600).unwrap();
918        let result = validate_token(&token);
919
920        assert!(result.is_ok());
921        let claims = result.unwrap();
922        assert_eq!(claims.sub, "test-id");
923        assert_eq!(claims.username, "testuser");
924        assert_eq!(claims.role, "viewer");
925        assert_eq!(claims.email, Some("test@example.com".to_string()));
926    }
927
928    #[test]
929    fn test_validate_token_invalid() {
930        let result = validate_token("invalid.token.here");
931        assert!(result.is_err());
932    }
933
934    #[test]
935    fn test_validate_token_expired() {
936        let user = User {
937            id: "test-id".to_string(),
938            username: "testuser".to_string(),
939            password_hash: "hash".to_string(),
940            role: UserRole::Editor,
941            email: None,
942        };
943
944        // Generate token that's already expired (2 minutes ago to exceed default leeway of 60s)
945        let token = generate_token(&user, -120).unwrap();
946        let result = validate_token(&token);
947
948        // Should fail validation due to expiration
949        assert!(result.is_err());
950    }
951
952    #[test]
953    fn test_claims_serialization() {
954        let claims = Claims {
955            sub: "user123".to_string(),
956            username: "testuser".to_string(),
957            role: "admin".to_string(),
958            email: Some("test@example.com".to_string()),
959            iat: 1234567890,
960            exp: 1234567890 + 3600,
961        };
962
963        let serialized = serde_json::to_string(&claims).unwrap();
964        let deserialized: Claims = serde_json::from_str(&serialized).unwrap();
965
966        assert_eq!(deserialized.sub, claims.sub);
967        assert_eq!(deserialized.username, claims.username);
968        assert_eq!(deserialized.role, claims.role);
969        assert_eq!(deserialized.email, claims.email);
970    }
971
972    #[test]
973    fn test_claims_to_user_context() {
974        let claims = Claims {
975            sub: "user123".to_string(),
976            username: "testuser".to_string(),
977            role: "editor".to_string(),
978            email: Some("test@example.com".to_string()),
979            iat: 1234567890,
980            exp: 1234567890 + 3600,
981        };
982
983        let context = claims_to_user_context(&claims);
984        assert_eq!(context.user_id, "user123");
985        assert_eq!(context.username, "testuser");
986        assert_eq!(context.role, UserRole::Editor);
987        assert_eq!(context.email, Some("test@example.com".to_string()));
988    }
989
990    #[test]
991    fn test_claims_to_user_context_unknown_role_defaults_to_viewer() {
992        let claims = Claims {
993            sub: "user123".to_string(),
994            username: "testuser".to_string(),
995            role: "unknown".to_string(),
996            email: None,
997            iat: 1234567890,
998            exp: 1234567890 + 3600,
999        };
1000
1001        let context = claims_to_user_context(&claims);
1002        assert_eq!(context.role, UserRole::Viewer);
1003    }
1004
1005    #[test]
1006    fn test_login_request_deserialization() {
1007        let json = r#"{"username": "testuser", "password": "testpass"}"#;
1008        let request: LoginRequest = serde_json::from_str(json).unwrap();
1009        assert_eq!(request.username, "testuser");
1010        assert_eq!(request.password, "testpass");
1011    }
1012
1013    #[test]
1014    fn test_refresh_token_request_deserialization() {
1015        let json = r#"{"refresh_token": "token123"}"#;
1016        let request: RefreshTokenRequest = serde_json::from_str(json).unwrap();
1017        assert_eq!(request.refresh_token, "token123");
1018    }
1019
1020    #[test]
1021    fn test_user_info_serialization() {
1022        let user_info = UserInfo {
1023            id: "user123".to_string(),
1024            username: "testuser".to_string(),
1025            role: "admin".to_string(),
1026            email: Some("test@example.com".to_string()),
1027        };
1028
1029        let serialized = serde_json::to_string(&user_info).unwrap();
1030        assert!(serialized.contains("user123"));
1031        assert!(serialized.contains("testuser"));
1032        assert!(serialized.contains("admin"));
1033    }
1034
1035    #[test]
1036    fn test_login_response_serialization() {
1037        let user_info = UserInfo {
1038            id: "user123".to_string(),
1039            username: "testuser".to_string(),
1040            role: "editor".to_string(),
1041            email: None,
1042        };
1043
1044        let response = LoginResponse {
1045            token: "access.token.here".to_string(),
1046            refresh_token: "refresh.token.here".to_string(),
1047            user: user_info,
1048            expires_in: 3600,
1049        };
1050
1051        let serialized = serde_json::to_string(&response).unwrap();
1052        assert!(serialized.contains("access.token.here"));
1053        assert!(serialized.contains("refresh.token.here"));
1054        assert!(serialized.contains("3600"));
1055    }
1056
1057    #[tokio::test]
1058    async fn test_rate_limiter_creation() {
1059        let limiter = RateLimiter::new(5, 60);
1060        let result = limiter.check_rate_limit("test-key").await;
1061        assert!(result.is_ok());
1062    }
1063
1064    #[tokio::test]
1065    async fn test_rate_limiter_exceeds_limit() {
1066        let limiter = RateLimiter::new(3, 60);
1067
1068        // First 3 attempts should succeed
1069        for _ in 0..3 {
1070            assert!(limiter.check_rate_limit("test-key").await.is_ok());
1071        }
1072
1073        // 4th attempt should fail
1074        let result = limiter.check_rate_limit("test-key").await;
1075        assert!(result.is_err());
1076        assert!(result.unwrap_err().contains("Too many"));
1077    }
1078
1079    #[tokio::test]
1080    async fn test_rate_limiter_reset() {
1081        let limiter = RateLimiter::new(3, 60);
1082
1083        // Use up the limit
1084        for _ in 0..3 {
1085            limiter.check_rate_limit("test-key").await.ok();
1086        }
1087
1088        // Reset
1089        limiter.reset_rate_limit("test-key").await;
1090
1091        // Should be able to make requests again
1092        let result = limiter.check_rate_limit("test-key").await;
1093        assert!(result.is_ok());
1094    }
1095
1096    #[tokio::test]
1097    async fn test_rate_limiter_different_keys() {
1098        let limiter = RateLimiter::new(2, 60);
1099
1100        // Use up limit for key1
1101        for _ in 0..2 {
1102            limiter.check_rate_limit("key1").await.ok();
1103        }
1104
1105        // key2 should still work
1106        let result = limiter.check_rate_limit("key2").await;
1107        assert!(result.is_ok());
1108
1109        // key1 should be limited
1110        let result = limiter.check_rate_limit("key1").await;
1111        assert!(result.is_err());
1112    }
1113
1114    #[tokio::test]
1115    async fn test_account_lockout_creation() {
1116        let lockout = AccountLockout::new(3, 900);
1117        let is_locked = lockout.is_locked("test-user").await;
1118        assert!(!is_locked);
1119    }
1120
1121    #[tokio::test]
1122    async fn test_account_lockout_record_failure() {
1123        let lockout = AccountLockout::new(3, 900);
1124
1125        for _ in 0..2 {
1126            lockout.record_failure("test-user").await;
1127        }
1128
1129        let is_locked = lockout.is_locked("test-user").await;
1130        assert!(!is_locked, "Should not be locked after 2 failures");
1131
1132        lockout.record_failure("test-user").await;
1133        let is_locked = lockout.is_locked("test-user").await;
1134        assert!(is_locked, "Should be locked after 3 failures");
1135    }
1136
1137    #[tokio::test]
1138    async fn test_account_lockout_reset() {
1139        let lockout = AccountLockout::new(2, 900);
1140
1141        // Lock the account
1142        for _ in 0..2 {
1143            lockout.record_failure("test-user").await;
1144        }
1145
1146        assert!(lockout.is_locked("test-user").await);
1147
1148        // Reset
1149        lockout.reset("test-user").await;
1150
1151        assert!(!lockout.is_locked("test-user").await);
1152    }
1153
1154    #[tokio::test]
1155    async fn test_account_lockout_remaining_time() {
1156        let lockout = AccountLockout::new(2, 5); // 5 second lockout
1157
1158        // Lock the account
1159        for _ in 0..2 {
1160            lockout.record_failure("test-user").await;
1161        }
1162
1163        let remaining = lockout.remaining_lockout_time("test-user").await;
1164        assert!(remaining.is_some());
1165        let time = remaining.unwrap();
1166        assert!(time > 0 && time <= 5);
1167    }
1168
1169    #[tokio::test]
1170    async fn test_global_user_store_initialization() {
1171        let store1 = init_global_user_store();
1172        let store2 = get_global_user_store();
1173
1174        assert!(store2.is_some());
1175
1176        // Both should be the same instance
1177        let store2 = store2.unwrap();
1178        assert!(Arc::ptr_eq(&store1, &store2));
1179    }
1180
1181    #[tokio::test]
1182    async fn test_all_default_users_exist() {
1183        let store = UserStore::new();
1184        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1185
1186        // Test admin
1187        let result = store.authenticate("admin", "admin123").await;
1188        assert!(result.is_ok());
1189        assert!(matches!(result.unwrap().role, UserRole::Admin));
1190
1191        // Test viewer
1192        let result = store.authenticate("viewer", "viewer123").await;
1193        assert!(result.is_ok());
1194        assert!(matches!(result.unwrap().role, UserRole::Viewer));
1195
1196        // Test editor
1197        let result = store.authenticate("editor", "editor123").await;
1198        assert!(result.is_ok());
1199        assert!(matches!(result.unwrap().role, UserRole::Editor));
1200    }
1201
1202    #[tokio::test]
1203    async fn test_concurrent_authentication_attempts() {
1204        let store = Arc::new(UserStore::new());
1205        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
1206
1207        let mut handles = vec![];
1208
1209        // Spawn multiple concurrent authentication attempts
1210        for i in 0..10 {
1211            let store_clone = store.clone();
1212            let handle = tokio::spawn(async move {
1213                if i % 2 == 0 {
1214                    store_clone.authenticate("admin", "admin123").await
1215                } else {
1216                    store_clone.authenticate("viewer", "viewer123").await
1217                }
1218            });
1219            handles.push(handle);
1220        }
1221
1222        // All should succeed
1223        for handle in handles {
1224            let result = handle.await.unwrap();
1225            assert!(result.is_ok());
1226        }
1227    }
1228}