openserve 2.0.3

A modern, high-performance, AI-enhanced file server built in Rust
Documentation
//! Authentication Service
//
//! This service handles all authentication-related business logic,
//! including user management, password hashing, and token generation.

use anyhow::Result;
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::RwLock;
use uuid::Uuid;
use std::sync::Arc;
use crate::config::Config;

use crate::{

    utils::crypto,
};

/// A service for handling authentication and user management.
pub struct AuthService {
    auth_config: LocalAuthConfig,
    sessions: RwLock<HashMap<String, Session>>,
}

/// Represents the claims in a JSON Web Token (JWT).
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    /// The subject of the token (usually the user ID).
    pub sub: String,
    /// The expiration time of the token (as a Unix timestamp).
    pub exp: i64,
    /// The time the token was issued at (as a Unix timestamp).
    pub iat: i64,
    /// The roles assigned to the user.
    pub roles: Vec<String>,
}

/// Represents an active user session.
#[derive(Debug, Clone)]
pub struct Session {
    /// The ID of the user associated with the session.
    pub user_id: String,
    /// The username of the user.
    pub username: String,
    /// The roles of the user.
    pub roles: Vec<String>,
    /// The time the session was created.
    pub created_at: chrono::DateTime<Utc>,
    /// The time the session expires.
    pub expires_at: chrono::DateTime<Utc>,
}

/// Represents a user account in the system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    /// The unique identifier for the user.
    pub id: String,
    /// The username for authentication.
    pub username: String,
    /// The user's email address.
    pub email: String,
    /// The hashed password for the user.
    pub password_hash: String,
    /// The roles assigned to the user.
    pub roles: Vec<String>,
    /// The timestamp of when the user account was created.
    pub created_at: chrono::DateTime<Utc>,
    /// Whether the user account is active.
    pub is_active: bool,
}

/// Represents a request to log in.
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
    /// The username of the user trying to log in.
    pub username: String,
    /// The password of the user.
    pub password: String,
}

/// Represents a successful login response.
#[derive(Debug, Serialize)]
pub struct LoginResponse {
    /// The JWT token for the authenticated session.
    pub token: String,
    /// The duration in seconds until the token expires.
    pub expires_in: i64,
    /// Information about the authenticated user.
    pub user: UserInfo,
}

/// Represents user information that is safe to expose to the client.
#[derive(Debug, Serialize)]
pub struct UserInfo {
    /// The unique identifier for the user.
    pub id: String,
    /// The username of the user.
    pub username: String,
    /// The user's email address.
    pub email: String,
    /// The roles assigned to the user.
    pub roles: Vec<String>,
}

/// Represents a request to register a new user.
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
    /// The desired username for the new user.
    pub username: String,
    /// The email address for the new user.
    pub email: String,
    /// The password for the new user.
    pub password: String,
}

struct LocalAuthConfig {
    #[allow(dead_code)]
    jwt_secret: String,
    session_timeout: u64,
    allow_registration: bool,
    encoding_key: EncodingKey,
    decoding_key: DecodingKey,
}

impl std::fmt::Debug for LocalAuthConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LocalAuthConfig")
            .field("session_timeout", &self.session_timeout)
            .field("allow_registration", &self.allow_registration)
            .finish_non_exhaustive()
    }
}

impl AuthService {
    /// Creates a new `AuthService`.
    pub async fn new(config: Arc<Config>) -> Result<Self> {
        let encoding_key = EncodingKey::from_secret(config.auth.jwt_secret.as_ref());
        let decoding_key = DecodingKey::from_secret(config.auth.jwt_secret.as_ref());

        let auth_service = Self {
            auth_config: LocalAuthConfig {
                jwt_secret: config.auth.jwt_secret.clone(),
                session_timeout: config.auth.token_expiration,
                allow_registration: config.auth.allow_registration,
                encoding_key,
                decoding_key,
            },
            sessions: RwLock::new(HashMap::new()),
        };

        Ok(auth_service)
    }

    /// Authenticate user and return JWT token
    pub async fn login(&self, request: LoginRequest) -> Result<LoginResponse> {
        // In a real implementation, this would query the database
        let user = self.find_user_by_username(&request.username).await?;
        
        if !user.is_active {
            return Err(anyhow::anyhow!("User account is disabled"));
        }

        if !crypto::verify_password(&request.password, &user.password_hash)? {
            return Err(anyhow::anyhow!("Invalid credentials"));
        }

        let token = self.generate_token(&user)?;
        let expires_in = self.auth_config.session_timeout as i64;

        // Create session
        let session = Session {
            user_id: user.id.clone(),
            username: user.username.clone(),
            roles: user.roles.clone(),
            created_at: Utc::now(),
            expires_at: Utc::now() + Duration::seconds(expires_in),
        };

        self.sessions.write().await.insert(user.id.clone(), session);

        Ok(LoginResponse {
            token,
            expires_in,
            user: UserInfo {
                id: user.id,
                username: user.username,
                email: user.email,
                roles: user.roles,
            },
        })
    }

    /// Register a new user
    pub async fn register(&self, request: RegisterRequest) -> Result<UserInfo> {
        if !self.auth_config.allow_registration {
            return Err(anyhow::anyhow!("Registration is disabled"));
        }

        // Validate input
        if !crate::utils::validation::is_valid_email(&request.email) {
            return Err(anyhow::anyhow!("Invalid email format"));
        }

        if request.password.len() < 8 {
            return Err(anyhow::anyhow!("Password must be at least 8 characters"));
        }

        // Check if user already exists
        if self.find_user_by_username(&request.username).await.is_ok() {
            return Err(anyhow::anyhow!("Username already exists"));
        }

        // Hash password
        let password_hash = crypto::hash_password(&request.password)?;

        // Create user
        let user = User {
            id: Uuid::new_v4().to_string(),
            username: request.username,
            email: request.email,
            password_hash,
            roles: vec!["user".to_string()],
            created_at: Utc::now(),
            is_active: true,
        };

        // In a real implementation, save to database
        self.save_user(&user).await?;

        Ok(UserInfo {
            id: user.id,
            username: user.username,
            email: user.email,
            roles: user.roles,
        })
    }

    /// Validate JWT token and return claims
    pub fn validate_token(&self, token: &str) -> Result<Claims> {
        let validation = Validation::new(Algorithm::HS256);
        let token_data = decode::<Claims>(token, &self.auth_config.decoding_key, &validation)?;
        
        // Check if token is expired
        let now = Utc::now().timestamp();
        if token_data.claims.exp < now {
            return Err(anyhow::anyhow!("Token has expired"));
        }

        Ok(token_data.claims)
    }

    /// Get session by user ID
    pub async fn get_session(&self, user_id: &str) -> Option<Session> {
        let sessions = self.sessions.read().await;
        sessions.get(user_id).cloned()
    }

    /// Logout user (invalidate session)
    pub async fn logout(&self, user_id: &str) -> Result<()> {
        self.sessions.write().await.remove(user_id);
        Ok(())
    }

    /// Check if user has required role
    pub fn has_role(&self, claims: &Claims, required_role: &str) -> bool {
        claims.roles.contains(&required_role.to_string()) || 
        claims.roles.contains(&"admin".to_string())
    }

    /// Clean up expired sessions
    pub async fn cleanup_expired_sessions(&self) {
        let now = Utc::now();
        let mut sessions = self.sessions.write().await;
        sessions.retain(|_, session| session.expires_at > now);
    }

    // Helper methods

    /// Generate JWT token for user
    fn generate_token(&self, user: &User) -> Result<String> {
        let now = Utc::now();
        let expires_at = now + Duration::seconds(self.auth_config.session_timeout as i64);

        let claims = Claims {
            sub: user.id.clone(),
            exp: expires_at.timestamp(),
            iat: now.timestamp(),
            roles: user.roles.clone(),
        };

        let token = encode(&Header::default(), &claims, &self.auth_config.encoding_key)?;
        Ok(token)
    }

    /// Find user by username (mock implementation)
    async fn find_user_by_username(&self, username: &str) -> Result<User> {
        // Mock user for testing
        if username == "admin" {
            return Ok(User {
                id: "admin-id".to_string(),
                username: "admin".to_string(),
                email: "admin@example.com".to_string(),
                password_hash: crypto::hash_password("admin123")?,
                roles: vec!["admin".to_string()],
                created_at: Utc::now(),
                is_active: true,
            });
        }

        Err(anyhow::anyhow!("User not found"))
    }

    /// Save user (mock implementation)
    async fn save_user(&self, _user: &User) -> Result<()> {
        // In a real implementation, this would save to database
        Ok(())
    }
}

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

    fn create_test_auth_service() -> AuthService {
        let config = LocalAuthConfig {
            jwt_secret: "test_secret_key_that_is_long_enough".to_string(),
            session_timeout: 3600,
            allow_registration: true,
            encoding_key: EncodingKey::from_secret("test_secret_key_that_is_long_enough".as_ref()),
            decoding_key: DecodingKey::from_secret("test_secret_key_that_is_long_enough".as_ref()),
        };
        AuthService {
            auth_config: config,
            sessions: RwLock::new(HashMap::new()),
        }
    }

    #[tokio::test]
    async fn test_login_success() {
        let auth_service = create_test_auth_service();
        
        let request = LoginRequest {
            username: "admin".to_string(),
            password: "admin123".to_string(),
        };

        let result = auth_service.login(request).await;
        assert!(result.is_ok());
        
        let response = result.unwrap();
        assert!(!response.token.is_empty());
        assert_eq!(response.user.username, "admin");
    }

    #[tokio::test]
    async fn test_token_validation() {
        let auth_service = create_test_auth_service();
        
        let user = User {
            id: "test-id".to_string(),
            username: "test".to_string(),
            email: "test@example.com".to_string(),
            password_hash: "hash".to_string(),
            roles: vec!["user".to_string()],
            created_at: Utc::now(),
            is_active: true,
        };

        let token = auth_service.generate_token(&user).unwrap();
        let claims = auth_service.validate_token(&token).unwrap();
        
        assert_eq!(claims.sub, "test-id");
        assert_eq!(claims.roles, vec!["user"]);
    }

    #[test]
    fn test_role_checking() {
        let auth_service = create_test_auth_service();
        
        let claims = Claims {
            sub: "user-id".to_string(),
            exp: Utc::now().timestamp() + 3600,
            iat: Utc::now().timestamp(),
            roles: vec!["user".to_string()],
        };

        assert!(auth_service.has_role(&claims, "user"));
        assert!(!auth_service.has_role(&claims, "admin"));
    }
}