Skip to main content

communitas_core/
auth_service.rs

1//! Shared Authentication Service
2//!
3//! This module provides a unified authentication service that can be used by all UI frontends
4//! (Tauri Desktop, TUI, Web, etc.). It encapsulates all business logic for:
5//! - Multi-identity management
6//! - Vault creation and authentication
7//! - Passkey/biometric support
8//! - Session management
9//! - Auto-login functionality
10
11use crate::encrypted_storage::{
12    EncryptedStorageManager, PasskeyInfo, RecentIdentity, Session, VaultInfo,
13};
14use anyhow::{Result, anyhow};
15use serde::{Deserialize, Serialize};
16
17/// Session information for active authenticated user
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SessionInfo {
20    pub session_id: String,
21    pub four_words: String,
22    pub display_name: String,
23}
24
25impl From<Session> for SessionInfo {
26    fn from(session: Session) -> Self {
27        Self {
28            session_id: session.id,
29            four_words: session.four_words,
30            display_name: session.display_name,
31        }
32    }
33}
34
35/// Unified authentication service for all UI frontends
36pub struct AuthService {
37    storage_manager: EncryptedStorageManager,
38    active_session: Option<Session>,
39}
40
41impl AuthService {
42    /// Create new auth service with storage manager
43    pub fn new(storage_manager: EncryptedStorageManager) -> Self {
44        Self {
45            storage_manager,
46            active_session: None,
47        }
48    }
49
50    /// Get reference to storage manager
51    pub fn storage_manager(&self) -> &EncryptedStorageManager {
52        &self.storage_manager
53    }
54
55    /// Get mutable reference to storage manager
56    pub fn storage_manager_mut(&mut self) -> &mut EncryptedStorageManager {
57        &mut self.storage_manager
58    }
59
60    /// Create a new vault for a four-word identity
61    ///
62    /// This creates an encrypted vault with PBKDF2 key derivation (100,000 iterations)
63    /// and ChaCha20-Poly1305 encryption.
64    pub async fn create_vault(
65        &mut self,
66        four_words: &str,
67        password: &str,
68        display_name: &str,
69    ) -> Result<String> {
70        tracing::info!("AuthService: Creating vault for {}", four_words);
71
72        let vault_id = self
73            .storage_manager
74            .create_vault(four_words, password, display_name)
75            .await?;
76
77        tracing::info!("AuthService: Vault created with ID: {}", vault_id);
78        Ok(vault_id)
79    }
80
81    /// Login with four-word identity and password
82    ///
83    /// On success, stores session and optionally saves password to keyring for auto-login.
84    pub async fn login(
85        &mut self,
86        four_words: &str,
87        password: &str,
88        _device_name: Option<&str>,
89    ) -> Result<SessionInfo> {
90        tracing::info!("AuthService: Login attempt for {}", four_words);
91
92        // Note: EncryptedStorageManager::login expects Option<Vec<u8>> for passkey, we pass None
93        let session = self
94            .storage_manager
95            .login(four_words, password, None)
96            .await?;
97
98        let session_info = SessionInfo::from(session.clone());
99        self.active_session = Some(session);
100
101        tracing::info!("AuthService: Login successful for {}", four_words);
102        Ok(session_info)
103    }
104
105    /// Logout current session
106    pub async fn logout(&mut self) -> Result<()> {
107        if let Some(session) = &self.active_session {
108            tracing::info!("AuthService: Logging out {}", session.four_words);
109            self.storage_manager.logout(&session.id).await?;
110            self.active_session = None;
111            tracing::info!("AuthService: Logout successful");
112            Ok(())
113        } else {
114            Err(anyhow!("No active session to logout"))
115        }
116    }
117
118    /// Get current active session
119    pub fn get_current_session(&self) -> Option<SessionInfo> {
120        self.active_session.as_ref().map(|s| SessionInfo {
121            session_id: s.id.clone(),
122            four_words: s.four_words.clone(),
123            display_name: s.display_name.clone(),
124        })
125    }
126
127    /// Check if user is currently logged in
128    pub fn is_logged_in(&self) -> bool {
129        self.active_session.is_some()
130    }
131
132    /// List all available vaults
133    pub async fn list_vaults(&self) -> Result<Vec<VaultInfo>> {
134        self.storage_manager.list_vaults().await
135    }
136
137    /// Get recent identities (sorted by last used, max 10)
138    pub async fn get_recent_identities(&self) -> Result<Vec<RecentIdentity>> {
139        // Note: storage_manager returns Vec directly, not Result
140        Ok(self.storage_manager.get_recent_identities().await)
141    }
142
143    /// Remove a recent identity from the list (does not delete the vault)
144    pub async fn remove_recent_identity(&mut self, four_words: &str) -> Result<()> {
145        self.storage_manager
146            .remove_recent_identity(four_words)
147            .await
148    }
149
150    /// Check if vault exists for four-word identity
151    pub async fn vault_exists(&self, four_words: &str) -> Result<bool> {
152        self.storage_manager.vault_exists(four_words).await
153    }
154
155    /// Delete a vault (requires password confirmation)
156    pub async fn delete_vault(&mut self, four_words: &str, password: &str) -> Result<()> {
157        tracing::warn!("AuthService: Deleting vault for {}", four_words);
158
159        // Verify password before deletion
160        let _ = self.login(four_words, password, None).await?;
161
162        self.storage_manager.delete_vault(four_words).await?;
163
164        // Logout if this was the active session
165        if let Some(session) = &self.active_session
166            && session.four_words == four_words
167        {
168            self.active_session = None;
169        }
170
171        tracing::warn!("AuthService: Vault deleted for {}", four_words);
172        Ok(())
173    }
174
175    // ========================================================================
176    // Passkey / Biometric Authentication Methods
177    // ========================================================================
178
179    /// Register a passkey for biometric authentication (legacy - without WebAuthn)
180    ///
181    /// This enables Touch ID, Face ID, or Windows Hello for the identity.
182    /// The password is stored in the platform keyring for secure retrieval.
183    pub async fn passkey_register(
184        &mut self,
185        four_words: &str,
186        device_name: &str,
187    ) -> Result<PasskeyInfo> {
188        tracing::info!(
189            "AuthService: Registering passkey for {} on {}",
190            four_words,
191            device_name
192        );
193
194        let info = self
195            .storage_manager
196            .passkey_register(four_words, device_name)
197            .await?;
198
199        tracing::info!("AuthService: Passkey registered successfully");
200        Ok(info)
201    }
202
203    /// Register a passkey with WebAuthn credential
204    ///
205    /// This stores the WebAuthn credential for true biometric authentication.
206    pub async fn passkey_register_webauthn(
207        &mut self,
208        four_words: &str,
209        device_name: &str,
210        credential: crate::encrypted_storage::passkey::WebAuthnCredential,
211    ) -> Result<PasskeyInfo> {
212        tracing::info!(
213            "AuthService: Registering WebAuthn passkey for {} on {}",
214            four_words,
215            device_name
216        );
217
218        let info = self
219            .storage_manager
220            .passkey_register_webauthn(four_words, device_name, credential)
221            .await?;
222
223        tracing::info!("AuthService: WebAuthn passkey registered successfully");
224        Ok(info)
225    }
226
227    /// Authenticate using passkey/biometric
228    ///
229    /// This retrieves the password from keyring and performs standard vault login.
230    pub async fn passkey_authenticate(&mut self, four_words: &str) -> Result<SessionInfo> {
231        tracing::info!("AuthService: Passkey authentication for {}", four_words);
232
233        let session = self
234            .storage_manager
235            .passkey_authenticate(four_words)
236            .await?;
237
238        let session_info = SessionInfo::from(session.clone());
239        self.active_session = Some(session);
240
241        tracing::info!("AuthService: Passkey authentication successful");
242        Ok(session_info)
243    }
244
245    /// Check if identity has a registered passkey
246    pub async fn passkey_has_passkey(&self, four_words: &str) -> Result<bool> {
247        Ok(self.storage_manager.passkey_has_passkey(four_words).await)
248    }
249
250    /// Get passkey information for an identity
251    pub async fn passkey_get_info(&self, four_words: &str) -> Result<PasskeyInfo> {
252        self.storage_manager.passkey_get_info(four_words).await
253    }
254
255    /// Delete passkey for an identity
256    pub async fn passkey_delete(&mut self, four_words: &str) -> Result<()> {
257        tracing::warn!("AuthService: Deleting passkey for {}", four_words);
258        self.storage_manager.passkey_delete(four_words).await?;
259        tracing::warn!("AuthService: Passkey deleted");
260        Ok(())
261    }
262
263    // ========================================================================
264    // Auto-Login Methods
265    // ========================================================================
266
267    /// Attempt auto-login using last-used identity
268    ///
269    /// Returns session info if successful, None if no auto-login available.
270    pub async fn try_auto_login(&mut self) -> Result<Option<SessionInfo>> {
271        tracing::info!("AuthService: Attempting auto-login");
272
273        // Get last used identity from app config (returns Vec directly, not Result)
274        let recent = self.storage_manager.get_recent_identities().await;
275
276        if recent.is_empty() {
277            tracing::info!("AuthService: No recent identities for auto-login");
278            return Ok(None);
279        }
280
281        let last_identity = &recent[0];
282        tracing::info!(
283            "AuthService: Attempting auto-login for {}",
284            last_identity.four_words
285        );
286
287        // Check if passkey is available
288        if last_identity.has_passkey {
289            match self.passkey_authenticate(&last_identity.four_words).await {
290                Ok(session_info) => {
291                    tracing::info!("AuthService: Auto-login successful via passkey");
292                    return Ok(Some(session_info));
293                }
294                Err(e) => {
295                    tracing::warn!("AuthService: Passkey auto-login failed: {}", e);
296                    // Fall through to return None
297                }
298            }
299        }
300
301        tracing::info!("AuthService: No auto-login available");
302        Ok(None)
303    }
304
305    /// Enable auto-login for current session
306    ///
307    /// Stores password in keyring so passkey authentication can work.
308    pub async fn enable_auto_login(&mut self, password: &str) -> Result<()> {
309        let session = self
310            .active_session
311            .as_ref()
312            .ok_or_else(|| anyhow!("No active session"))?;
313
314        tracing::info!(
315            "AuthService: Enabling auto-login for {}",
316            session.four_words
317        );
318
319        // Store password in keyring via storage manager
320        self.storage_manager
321            .store_password_in_keyring(&session.four_words, password)
322            .await?;
323
324        tracing::info!("AuthService: Auto-login enabled");
325        Ok(())
326    }
327
328    /// Disable auto-login for an identity
329    pub async fn disable_auto_login(&mut self, four_words: &str) -> Result<()> {
330        tracing::info!("AuthService: Disabling auto-login for {}", four_words);
331
332        // Remove password from keyring
333        self.storage_manager
334            .remove_password_from_keyring(four_words)
335            .await?;
336
337        // Delete passkey if exists
338        if self.passkey_has_passkey(four_words).await? {
339            self.passkey_delete(four_words).await?;
340        }
341
342        tracing::info!("AuthService: Auto-login disabled");
343        Ok(())
344    }
345
346    // ========================================================================
347    // Identity Switching Methods
348    // ========================================================================
349
350    /// Switch to another identity (logout current, login new)
351    pub async fn switch_identity(&mut self, four_words: &str) -> Result<SessionInfo> {
352        tracing::info!("AuthService: Switching to identity {}", four_words);
353
354        // Logout current session if exists
355        if self.active_session.is_some() {
356            self.logout().await.ok(); // Ignore logout errors
357        }
358
359        // Try passkey authentication first
360        if self.passkey_has_passkey(four_words).await? {
361            match self.passkey_authenticate(four_words).await {
362                Ok(session_info) => {
363                    tracing::info!("AuthService: Identity switch successful via passkey");
364                    return Ok(session_info);
365                }
366                Err(e) => {
367                    tracing::warn!("AuthService: Passkey switch failed: {}", e);
368                    return Err(anyhow!("Passkey authentication required but failed"));
369                }
370            }
371        }
372
373        Err(anyhow!(
374            "Cannot switch to identity without password or passkey"
375        ))
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use crate::encrypted_storage::StorageConfig;
383    use tempfile::TempDir;
384
385    #[tokio::test]
386    async fn test_auth_service_basic_flow() {
387        let temp_dir = TempDir::new().expect("Failed to create temp dir");
388        let config = StorageConfig {
389            vault_dir: temp_dir.path().to_path_buf(),
390            use_keyring: false,
391            ..Default::default()
392        };
393
394        let storage_manager = EncryptedStorageManager::new(config)
395            .await
396            .expect("Failed to create storage manager");
397
398        let mut auth_service = AuthService::new(storage_manager);
399
400        // Create vault
401        let vault_id = auth_service
402            .create_vault("ocean-forest-moon-star", "test-password", "Test User")
403            .await
404            .expect("Failed to create vault");
405
406        assert!(!vault_id.is_empty());
407
408        // Login
409        let session_info = auth_service
410            .login(
411                "ocean-forest-moon-star",
412                "test-password",
413                Some("Test Device"),
414            )
415            .await
416            .expect("Failed to login");
417
418        assert_eq!(session_info.four_words, "ocean-forest-moon-star");
419        assert_eq!(session_info.display_name, "Test User");
420        assert!(auth_service.is_logged_in());
421
422        // Get current session
423        let current = auth_service.get_current_session();
424        assert!(current.is_some());
425        assert_eq!(current.unwrap().four_words, "ocean-forest-moon-star");
426
427        // Logout
428        auth_service.logout().await.expect("Failed to logout");
429        assert!(!auth_service.is_logged_in());
430    }
431
432    #[tokio::test]
433    async fn test_auth_service_recent_identities() {
434        let temp_dir = TempDir::new().expect("Failed to create temp dir");
435        let vault_subdir = temp_dir.path().join("vaults");
436        std::fs::create_dir_all(&vault_subdir).expect("Failed to create vault subdir");
437        let config = StorageConfig {
438            vault_dir: vault_subdir,
439            use_keyring: false,
440            ..Default::default()
441        };
442
443        let storage_manager = EncryptedStorageManager::new(config)
444            .await
445            .expect("Failed to create storage manager");
446
447        let mut auth_service = AuthService::new(storage_manager);
448
449        // Create and login with first identity
450        auth_service
451            .create_vault("ocean-forest-moon-star", "pass1", "User 1")
452            .await
453            .expect("Failed to create vault 1");
454
455        auth_service
456            .login("ocean-forest-moon-star", "pass1", Some("Device 1"))
457            .await
458            .expect("Failed to login 1");
459
460        auth_service.logout().await.expect("Failed to logout 1");
461
462        // Create and login with second identity
463        auth_service
464            .create_vault("river-cloud-stone-tree", "pass2", "User 2")
465            .await
466            .expect("Failed to create vault 2");
467
468        auth_service
469            .login("river-cloud-stone-tree", "pass2", Some("Device 2"))
470            .await
471            .expect("Failed to login 2");
472
473        // Get recent identities
474        let recent = auth_service
475            .get_recent_identities()
476            .await
477            .expect("Failed to get recent");
478
479        assert_eq!(recent.len(), 2);
480        // Most recent should be first
481        assert_eq!(recent[0].four_words, "river-cloud-stone-tree");
482        assert_eq!(recent[1].four_words, "ocean-forest-moon-star");
483    }
484}