//! Authentication and Authorization Module //! //! Comprehensive authentication system with JWT tokens, password hashing, //! user management, and Cedar policy integration. //! //! # Architecture //! //! ```text //! ┌─────────────────────────────────────────┐ //! │ Authentication Module │ //! ├─────────────────────────────────────────┤ //! │ │ //! │ ┌──────────────┐ ┌─────────────────┐ │ //! │ │ JWT Service │ │ Password Service│ │ //! │ │ │ │ │ │ //! │ │ • Generate │ │ • Hash (Argon2) │ │ //! │ │ • Validate │ │ • Verify │ │ //! │ │ • Rotate │ │ • Strength │ │ //! │ │ • Revoke │ └─────────────────┘ │ //! │ └──────────────┘ │ //! │ │ │ //! │ ▼ │ //! │ ┌──────────────┐ │ //! │ │ User Service │ │ //! │ │ │ │ //! │ │ • Create │ │ //! │ │ • Login │ │ //! │ │ • Logout │ │ //! │ │ • Update │ │ //! │ └──────────────┘ │ //! └─────────────────────────────────────────┘ //! ``` //! //! # Token Flow //! //! ```text //! 1. Login Request //! ↓ //! 2. Validate Credentials (Password Service) //! ↓ //! 3. Generate Token Pair (JWT Service) //! ├─ Access Token (15 min) //! └─ Refresh Token (7 days) //! ↓ //! 4. Return Token Pair //! ↓ //! 5. Client Uses Access Token //! ↓ //! 6. Access Token Expires //! ↓ //! 7. Rotate Using Refresh Token //! ↓ //! 8. New Token Pair Generated //! ``` //! //! # Security Features //! //! - **RS256 Asymmetric Signing**: Enhanced security over symmetric HMAC //! - **Token Rotation**: Automatic rotation before expiry //! - **Token Revocation**: Blacklist-based revocation //! - **Password Hashing**: Argon2id (memory-hard, side-channel resistant) //! - **Cedar Integration**: Permissions hash for quick validation //! - **Thread-Safe**: Arc + RwLock for concurrent access //! //! # Usage Example //! //! ```no_run //! # use control_center::auth::{JwtService, PasswordService, User}; //! # async fn example() -> Result<(), Box> { //! // Initialize services //! let private_key = std::fs::read("keys/private.pem")?; //! let public_key = std::fs::read("keys/public.pem")?; //! //! let jwt_service = JwtService::new( //! &private_key, //! &public_key, //! "control-center", //! vec!["orchestrator".to_string(), "cli".to_string()], //! )?; //! //! let password_service = PasswordService::new(); //! //! // Create user with hashed password //! let password_hash = password_service.hash_password("secure_password")?; //! let user = User { //! id: "user123".to_string(), //! username: "alice".to_string(), //! password_hash, //! // ... other fields //! }; //! //! // Login: verify password and generate tokens //! if password_service.verify_password("secure_password", &user.password_hash)? { //! let tokens = jwt_service.generate_token_pair( //! &user.id, //! "workspace1", //! "permissions_hash", //! None, //! )?; //! //! println!("Access token: {}", tokens.access_token); //! println!("Expires in: {} seconds", tokens.expires_in); //! } //! //! // Validate token //! let claims = jwt_service.validate_token(&tokens.access_token)?; //! println!("User: {}, Workspace: {}", claims.sub, claims.workspace); //! //! // Rotate token before expiry //! if claims.needs_rotation() { //! let new_tokens = jwt_service.rotate_token(&tokens.refresh_token)?; //! println!("New access token: {}", new_tokens.access_token); //! } //! //! // Revoke token (logout) //! jwt_service.revoke_token(&claims.jti, claims.exp)?; //! # Ok(()) //! # } //! ``` pub mod jwt; pub mod password; pub mod user; // Re-export commonly used types use std::collections::HashMap; use std::sync::Arc; pub use jwt::{BlacklistStats, JwtService, TokenClaims, TokenPair, TokenType}; pub use password::{PasswordService, PasswordStrength}; pub use user::{User, UserRole, UserService, UserStatus}; use crate::error::{auth, http, infrastructure, ControlCenterError, Result}; #[cfg(feature = "mfa")] use crate::mfa::MfaService; /// Unified authentication service combining JWT, password, and MFA management #[allow(clippy::manual_non_exhaustive)] pub struct AuthService { /// JWT token service pub jwt: Arc, /// Password management service pub password: Arc, /// User management service pub user: Arc, /// MFA service (optional) #[cfg(feature = "mfa")] pub mfa: Option>, /// MFA service placeholder when feature is disabled #[cfg(not(feature = "mfa"))] _mfa_placeholder: (), } impl AuthService { /// Create new authentication service /// /// # Arguments /// * `jwt_service` - JWT token service /// * `password_service` - Password management service /// * `user_service` - User management service pub fn new( jwt_service: JwtService, password_service: PasswordService, user_service: UserService, ) -> Self { #[cfg(feature = "mfa")] { Self { jwt: Arc::new(jwt_service), password: Arc::new(password_service), user: Arc::new(user_service), mfa: None, } } #[cfg(not(feature = "mfa"))] { Self { jwt: Arc::new(jwt_service), password: Arc::new(password_service), user: Arc::new(user_service), _mfa_placeholder: (), } } } /// Create new authentication service with MFA support #[cfg(feature = "mfa")] pub fn with_mfa( jwt_service: JwtService, password_service: PasswordService, user_service: UserService, mfa_service: MfaService, ) -> Self { Self { jwt: Arc::new(jwt_service), password: Arc::new(password_service), user: Arc::new(user_service), mfa: Some(Arc::new(mfa_service)), } } /// Authenticate user and generate token pair /// /// # Arguments /// * `username` - Username or email /// * `password` - Plain text password /// * `workspace` - Workspace identifier /// /// # Returns /// Token pair on successful authentication (or partial token if MFA /// required) pub async fn login( &self, username: &str, password: &str, workspace: &str, ) -> Result { // Find user let user = self.user.find_by_username(username).await?.ok_or_else(|| { ControlCenterError::Auth(auth::AuthError::Authentication( "Invalid credentials".to_string(), )) })?; // Check user status if user.status != UserStatus::Active { return Err(ControlCenterError::Auth(auth::AuthError::Authentication( format!("User account is {}", user.status), ))); } // Verify password if !self .password .verify_password(password, &user.password_hash)? { return Err(ControlCenterError::Auth(auth::AuthError::Authentication( "Invalid credentials".to_string(), ))); } // Check if MFA is required #[cfg(feature = "mfa")] { if let Some(ref mfa_service) = self.mfa { if mfa_service.user_has_mfa(&user.id).await? { // Return partial token - user must complete MFA // The token has limited permissions and expires in 5 minutes let mut metadata = HashMap::new(); metadata.insert("mfa_required".to_string(), "true".to_string()); metadata.insert("ttl".to_string(), "300".to_string()); return self.jwt.generate_token_pair( &user.id, workspace, "mfa_pending", Some(metadata), ); } } } // Update last login self.user.update_last_login(&user.id).await?; // Generate permissions hash from user roles let permissions_hash = self.generate_permissions_hash(&user.roles); // Generate token pair self.jwt .generate_token_pair(&user.id, workspace, &permissions_hash, None) } /// Complete MFA verification and upgrade partial token to full access /// /// # Arguments /// * `partial_token` - Partial access token from initial login /// * `mfa_code` - MFA verification code (TOTP or backup code) /// /// # Returns /// Full access token pair on successful MFA verification #[cfg(feature = "mfa")] pub async fn complete_mfa_login( &self, partial_token: &str, mfa_code: &str, ) -> Result { // Validate partial token let claims = self.jwt.validate_token(partial_token)?; // Verify it's a pending MFA token if claims.permissions_hash != "mfa_pending" { return Err(ControlCenterError::Auth(auth::AuthError::Authentication( "Invalid MFA token".to_string(), ))); } // Get MFA service let mfa_service = self.mfa.as_ref().ok_or_else(|| { ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Internal( "MFA not configured".to_string(), )) })?; // Get user for email let user = self.user.find_by_id(&claims.sub).await?.ok_or_else(|| { ControlCenterError::Http(http::HttpError::NotFound("User not found".to_string())) })?; // Verify MFA code let verification = mfa_service .verify_any_mfa(&user.id, &user.email, mfa_code) .await?; if !verification.verified { return Err(ControlCenterError::Auth(auth::AuthError::Authentication( "Invalid MFA code".to_string(), ))); } // Revoke partial token self.jwt.revoke_token(&claims.jti, claims.exp)?; // Update last login self.user.update_last_login(&user.id).await?; // Generate full permissions hash let permissions_hash = self.generate_permissions_hash(&user.roles); // Generate full access token pair self.jwt .generate_token_pair(&user.id, &claims.workspace, &permissions_hash, None) } /// Logout user by revoking tokens pub async fn logout(&self, access_token: &str) -> Result<()> { let claims = self.jwt.validate_token(access_token)?; self.jwt.revoke_token(&claims.jti, claims.exp)?; Ok(()) } /// Validate access token and return claims pub fn validate(&self, token: &str) -> Result { self.jwt.validate_token(token) } /// Rotate tokens using refresh token pub fn refresh(&self, refresh_token: &str) -> Result { self.jwt.rotate_token(refresh_token) } /// Generate permissions hash from user roles /// /// This creates a stable hash of the user's Cedar permissions /// for quick validation without policy evaluation fn generate_permissions_hash(&self, roles: &[UserRole]) -> String { use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); for role in roles { hasher.update(role.to_string().as_bytes()); } format!("{:x}", hasher.finalize()) } } #[cfg(test)] mod tests { use super::*; fn create_test_jwt_service() -> JwtService { // Pre-generated RSA keys to avoid runtime key generation (avoids rand_core // conflict) let private_pem = b"-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7F43HxrVfJJ+k DQMEjENGqJLnBn6MvJnCu93A4ZNKEEpPGX1Y6V+qiqLH5B7wNMIJ2QVnLjYjKZu5 K5LkVCH8N7N1s9QXiLI7TzQVEH2TQx3MzQFRmVzGlyVFxF0qMHf/mNlZvE0Hb9oI YdYnBW5k4TRmzrN4THEbWqLcGVVfMVFQqG1j/V3G5dTpV1sN8a9xqJG0dKNmVP2M H5JG3kQ8CxFqp7pRsGvqH9IVjB6KXj8L3VPQYJxIBKHGZMlsP9SVqH3vwqLLKcBj G2UvhkVYJ6ZLvYIj7LvZKrCBrP3h5Q7hYpXZqDGYd5dDwWFJ3xkVMkJMGR3D3VrK R1jvJTEhAgMBAAECggEAXV2TZjfRbhR0IaqU3bpSZvb+6uL+5OgWRO5VbG0XGFmv 1v+2F5hQEZYKKxZvTlF4IxN1J0YQRKt4BG6PZqN9+Uj8z7DVEDqiWLIZSEhUPzVu E3IKZaEqvGKvEwBglmfqnE6cEZ2Kb9aOx1a5N8UH5q7xQ2d4YxKfYwRBkDYEZmZp R4tEb0qfKFgIGmCfK5fNvJqXvH/f1xq9QW1qrGYqGAqh7bkxvXGfkRBXh7Q8sAXW B2XQ8L9CZV5lsP0F7n8nKvDVKfqZfGXlZJZQvJPF5L8p6g7V7uE+6bZlF4YZLRvW 2aq2xZR6bGCWxBpLb2lG1QU0aJLqVmVjAQYDM9PB0QKBgQDfhKn8F5aITVFJhVVu BN8F2hLRtFDfSPTN0JM1WZq/EqYdLjnlAY8L7kBBzNaKgOGBRqfO1lVv7DG4vPtX nrLaM3SU3F0lC8tVwqJTUvZS5/Pl3wqAZRlWbWm4Yp6gWXlGPTaWRpGRBOqB+rWP b8rX2SnZjvFlQZlZPfvHGWpkFQKBgQDYtvXAGNdj8hMZ6mMUKYv3YbF7F2xUlf1x H0f8I5QGPiZoqzNGJXCEGvNVNEUgR7LY6AeqWtLqRKJsQ5NYb2XzLhA2BVOV1BV2 Z1Y3s0V5XjFE4q7gWmEcLlWyEiKiRCRQC4T7qB+gUQ8lFBrGqQ4vZqE+7pQyYfCt 1yFQnvqrQQKBgQCcvCvE4wMhF2lYFxM4v8qCDXJ9nLMKjYPFn3C0YrP6Zn2Pm7r3 fJGDVBmN5wVLDXqZnSrmh0iBs7PdBqeHALF7jTj9X8tNe+hpXwQ/GWSF8b3q1b6k wZLhKpJQGpYH0M3l7B6rrM3Vr1E/DQjXvXdqXrL3vKfHQrEk3c9zVCkfTQKBgGVl qdvBYvdgWHvPzL3+vqvLPVKDnLjN6hXA2cJGaBNpOlm+bI5cJLLnWhJrjCkK+POo 0oPKvnO5qKQjRDWLVCJH3z8MxPjqV6Yk1hAg/4vqV3oQsJDfU3qiDPAHrBbKBlRm 4KYmEqIhqNYJqXN7nnQFOLYFxOaXKqe6HBh1g4QBAoGBAIRXPwKJhwIUfLJ8DQAO 9z9YH3eIiGkLxF8f7WZvVnNwMEpXl4IqN8N3qGXKJfXJI1YfBKXN7LOqCOVFGkVq JkVN8qPTwAKU8E2U3dGzQVPP2JmGPPNyZJxQJ3sYtKnLkz2YdEbKDhkG5wPDHKKu LfEK1YMqL/VvAJdIXxqZqDXf -----END PRIVATE KEY-----"; let public_pem = b"-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxeNx8a1XySfpA0DBIxD RqiS5wZ+jLyZwrvdwOGTShBKTxl9WOlfqoqix+Qe8DTCCdkFZy42IymbhSuS5FQh /DezdLPUF4iyO080FRB9k0MdzM0BUZlcxpclRcRdKjB3/5jZWbxNB2/aCGHWJwVu ZOE0Zs6zeExxG1qi3BlVXzFRUKhtY/1dxuXU6Vdbl/GvcaiRtHSjZlT9jB+SRt5E PAsTaqe6UbBr6h/SFYweil4/C91T0GCcSAShxmTJbD/Ulah978KizynAYxtlL4ZF WCemS72CI+y72SqwgaZ94eUO4WKV2agxmHeXQ8FhSd8ZFTJCTBKDW91Kykdo7yUx IQIDAQAB -----END PUBLIC KEY-----"; JwtService::new( private_pem, public_pem, "test-issuer", vec!["test-audience".to_string()], ) .unwrap() } #[test] fn test_auth_service_creation() { let jwt_service = create_test_jwt_service(); let password_service = PasswordService::new(); let user_service = UserService::new(); let auth_service = AuthService::new(jwt_service, password_service, user_service); assert!(Arc::strong_count(&auth_service.jwt) >= 1); assert!(Arc::strong_count(&auth_service.password) >= 1); assert!(Arc::strong_count(&auth_service.user) >= 1); } #[test] fn test_permissions_hash_generation() { let jwt_service = create_test_jwt_service(); let password_service = PasswordService::new(); let user_service = UserService::new(); let auth_service = AuthService::new(jwt_service, password_service, user_service); let roles = vec![UserRole::Admin, UserRole::Developer]; let hash1 = auth_service.generate_permissions_hash(&roles); let hash2 = auth_service.generate_permissions_hash(&roles); // Hash should be deterministic assert_eq!(hash1, hash2); assert_eq!(hash1.len(), 64); // SHA256 hex = 64 chars } }