torii_auth_password/
lib.rs

1//! A plugin for Torii that provides email and password authentication.
2//!
3//! This plugin allows users to register and authenticate using an email address and password.
4//! It handles password hashing, validation.
5//!
6//! # Usage
7//!
8//! ```rust,no_run
9//! use torii_auth_password::PasswordPlugin;
10//! use torii_storage_sqlite::SqliteStorage;
11//! use torii_core::{PluginManager, DefaultUserManager};
12//! use std::sync::Arc;
13//!
14//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
15//! # let pool = sqlx::SqlitePool::connect("sqlite::memory:").await?;
16//! // Initialize storage
17//! let storage = Arc::new(SqliteStorage::new(pool.clone()));
18//! let session_storage = Arc::new(SqliteStorage::new(pool.clone()));
19//!
20//! // Create user manager
21//! let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
22//!
23//! // Create password plugin
24//! let plugin = PasswordPlugin::new(user_manager.clone(), storage.clone());
25//!
26//! // Register plugin with plugin manager
27//! let mut manager = PluginManager::new(storage.clone(), session_storage.clone());
28//! manager.register_plugin(plugin);
29//!
30//! // Register a new user
31//! let plugin = manager.get_plugin::<PasswordPlugin<DefaultUserManager<SqliteStorage>, SqliteStorage>>("password").unwrap();
32//! let user = plugin.register_user_with_password("user@example.com", "password123", None).await?;
33//!
34//! // Login an existing user
35//! let user = plugin.login_user_with_password("user@example.com", "password123").await?;
36//! # Ok(())
37//! # }
38//! ```
39//!
40//! The password plugin requires:
41//! 1. A UserManager implementation for user management
42//! 2. A storage implementation that implements the [`PasswordStorage`] trait
43//!
44//! # Features
45//!
46//! - User registration with email and password
47//! - Password hashing and validation
48//! - Optional email verification
49//! - Event emission for authentication events
50
51use std::sync::Arc;
52
53use chrono::{DateTime, Utc};
54use password_auth::{generate_hash, verify_password};
55use regex::Regex;
56use torii_core::{
57    Error, NewUser, Plugin, User, UserId, UserManager,
58    error::{AuthError, StorageError, ValidationError},
59    events::{Event, EventBus},
60    storage::PasswordStorage,
61};
62
63pub struct PasswordPlugin<M, S>
64where
65    M: UserManager,
66    S: PasswordStorage,
67{
68    user_manager: Arc<M>,
69    password_storage: Arc<S>,
70    event_bus: Option<EventBus>,
71}
72
73impl<M, S> PasswordPlugin<M, S>
74where
75    M: UserManager,
76    S: PasswordStorage,
77{
78    pub fn new(user_manager: Arc<M>, password_storage: Arc<S>) -> Self {
79        Self {
80            user_manager,
81            password_storage,
82            event_bus: None,
83        }
84    }
85
86    pub fn with_event_bus(mut self, event_bus: EventBus) -> Self {
87        self.event_bus = Some(event_bus);
88        self
89    }
90}
91
92impl<M, S> Plugin for PasswordPlugin<M, S>
93where
94    M: UserManager,
95    S: PasswordStorage,
96{
97    fn name(&self) -> String {
98        "password".to_string()
99    }
100}
101
102impl<M, S> PasswordPlugin<M, S>
103where
104    M: UserManager,
105    S: PasswordStorage,
106{
107    pub async fn register_user_with_password(
108        &self,
109        email: &str,
110        password: &str,
111        email_verified_at: Option<DateTime<Utc>>,
112    ) -> Result<User, Error> {
113        if !is_valid_email(email) {
114            return Err(Error::Validation(ValidationError::InvalidEmail));
115        }
116        if !is_valid_password(password) {
117            return Err(Error::Validation(ValidationError::WeakPassword));
118        }
119
120        if let Some(_user) = self.user_manager.get_user_by_email(email).await? {
121            tracing::debug!(email = %email, "User already exists");
122            return Err(Error::Auth(AuthError::UserAlreadyExists));
123        }
124
125        let new_user = NewUser::builder()
126            .email(email.to_string())
127            .email_verified_at(email_verified_at)
128            .build()
129            .unwrap();
130        let user = self.user_manager.create_user(&new_user).await?;
131
132        let hash = generate_hash(password);
133        self.password_storage
134            .set_password_hash(&user.id, &hash)
135            .await
136            .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;
137
138        tracing::info!(
139            user.id = %user.id,
140            user.email = %user.email,
141            user.name = ?user.name,
142            "Created user",
143        );
144
145        self.emit_event(&Event::UserCreated(user.clone())).await?;
146
147        Ok(user)
148    }
149
150    pub async fn change_user_password(
151        &self,
152        user_id: &UserId,
153        old_password: &str,
154        new_password: &str,
155    ) -> Result<(), Error> {
156        if !is_valid_password(new_password) {
157            return Err(Error::Validation(ValidationError::WeakPassword));
158        }
159
160        let user = self
161            .user_manager
162            .get_user(user_id)
163            .await?
164            .ok_or(Error::Auth(AuthError::UserNotFound))?;
165
166        let stored_hash = self
167            .password_storage
168            .get_password_hash(user_id)
169            .await
170            .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;
171
172        if stored_hash.is_none() {
173            return Err(Error::Auth(AuthError::InvalidCredentials));
174        }
175
176        verify_password(old_password, &stored_hash.unwrap())
177            .map_err(|_| Error::Auth(AuthError::InvalidCredentials))?;
178
179        let new_hash = generate_hash(new_password);
180        self.password_storage
181            .set_password_hash(user_id, &new_hash)
182            .await
183            .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;
184
185        tracing::info!(
186            user.id = %user_id,
187            "Changed user password",
188        );
189
190        self.emit_event(&Event::UserUpdated(user)).await?;
191
192        Ok(())
193    }
194
195    pub async fn login_user_with_password(
196        &self,
197        email: &str,
198        password: &str,
199    ) -> Result<User, Error> {
200        let user = self
201            .user_manager
202            .get_user_by_email(email)
203            .await?
204            .ok_or(Error::Auth(AuthError::UserNotFound))?;
205
206        if !user.is_email_verified() {
207            return Err(Error::Auth(AuthError::EmailNotVerified));
208        }
209
210        let hash = self
211            .password_storage
212            .get_password_hash(&user.id)
213            .await
214            .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;
215
216        if hash.is_none() {
217            return Err(Error::Auth(AuthError::InvalidCredentials));
218        }
219
220        verify_password(password, &hash.unwrap())
221            .map_err(|_| Error::Auth(AuthError::InvalidCredentials))?;
222
223        Ok(user)
224    }
225
226    async fn emit_event(&self, event: &Event) -> Result<(), Error> {
227        if let Some(event_bus) = &self.event_bus {
228            event_bus.emit(event).await?;
229        }
230        Ok(())
231    }
232}
233
234/// Validate an email address.
235fn is_valid_email(email: &str) -> bool {
236    let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
237    email_regex.is_match(email)
238}
239
240/// Validate a password.
241fn is_valid_password(password: &str) -> bool {
242    // TODO: Add more robust password validation
243    password.len() >= 8
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use async_trait::async_trait;
250    use sqlx::SqlitePool;
251    use std::sync::{
252        Arc,
253        atomic::{AtomicBool, AtomicUsize, Ordering},
254    };
255    use torii_core::{DefaultUserManager, error::EventError, events::EventHandler};
256    use torii_storage_sqlite::SqliteStorage;
257
258    async fn setup_storage() -> Result<Arc<SqliteStorage>, Error> {
259        let _ = tracing_subscriber::fmt().try_init();
260
261        let pool = SqlitePool::connect("sqlite::memory:")
262            .await
263            .expect("Failed to create pool");
264
265        let user_storage = Arc::new(SqliteStorage::new(pool.clone()));
266        user_storage.migrate().await?;
267
268        Ok(user_storage)
269    }
270
271    #[test]
272    fn test_is_valid_email() {
273        // Valid email addresses
274        assert!(is_valid_email("test@example.com"));
275        assert!(is_valid_email("user.name@domain.co.uk"));
276        assert!(is_valid_email("user+tag@example.com"));
277        assert!(is_valid_email("123@domain.com"));
278
279        // Invalid email addresses
280        assert!(!is_valid_email(""));
281        assert!(!is_valid_email("not-an-email"));
282        assert!(!is_valid_email("@domain.com"));
283        assert!(!is_valid_email("user@"));
284        assert!(!is_valid_email("user@.com"));
285        assert!(!is_valid_email("user@domain"));
286        assert!(!is_valid_email("user name@domain.com"));
287    }
288
289    #[test]
290    fn test_is_valid_password() {
291        // Valid passwords (>= 8 characters)
292        assert!(is_valid_password("password123"));
293        assert!(is_valid_password("12345678"));
294        assert!(is_valid_password("abcdefghijklmnop"));
295
296        // Invalid passwords (< 8 characters)
297        assert!(!is_valid_password(""));
298        assert!(!is_valid_password("short"));
299        assert!(!is_valid_password("1234567"));
300    }
301
302    #[tokio::test]
303    async fn test_create_user_and_login_with_unverified_email() -> Result<(), Error> {
304        let storage = setup_storage().await?;
305        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
306
307        let plugin = PasswordPlugin::new(user_manager.clone(), storage.clone())
308            .register_user_with_password("test@example.com", "password", None)
309            .await?;
310        assert_eq!(plugin.email, "test@example.com");
311
312        let result = PasswordPlugin::new(user_manager.clone(), storage.clone())
313            .login_user_with_password("test@example.com", "password")
314            .await;
315        assert!(matches!(
316            result,
317            Err(Error::Auth(AuthError::EmailNotVerified))
318        ));
319
320        Ok(())
321    }
322
323    #[tokio::test]
324    async fn test_create_user_and_login_with_verified_email() -> Result<(), Error> {
325        let storage = setup_storage().await?;
326        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
327
328        let plugin = PasswordPlugin::new(user_manager.clone(), storage.clone())
329            .register_user_with_password("test@example.com", "password", Some(Utc::now()))
330            .await?;
331        assert_eq!(plugin.email, "test@example.com");
332
333        let plugin = PasswordPlugin::new(user_manager.clone(), storage.clone())
334            .login_user_with_password("test@example.com", "password")
335            .await?;
336        assert_eq!(plugin.email, "test@example.com");
337
338        Ok(())
339    }
340
341    #[tokio::test]
342    async fn test_create_duplicate_user() -> Result<(), Error> {
343        let storage = setup_storage().await?;
344        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
345
346        let _ = PasswordPlugin::new(user_manager.clone(), storage.clone())
347            .register_user_with_password("test@example.com", "password", None)
348            .await?;
349
350        let result = PasswordPlugin::new(user_manager.clone(), storage.clone())
351            .register_user_with_password("test@example.com", "password", None)
352            .await;
353
354        assert!(matches!(
355            result,
356            Err(Error::Auth(AuthError::UserAlreadyExists))
357        ));
358
359        Ok(())
360    }
361
362    #[tokio::test]
363    async fn test_invalid_email_format() -> Result<(), Error> {
364        let storage = setup_storage().await?;
365        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
366
367        let result = PasswordPlugin::new(user_manager.clone(), storage.clone())
368            .register_user_with_password("not-an-email", "password", None)
369            .await;
370
371        assert!(matches!(
372            result,
373            Err(Error::Validation(ValidationError::InvalidEmail))
374        ));
375
376        Ok(())
377    }
378
379    #[tokio::test]
380    async fn test_weak_password() -> Result<(), Error> {
381        let storage = setup_storage().await?;
382        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
383
384        let result = PasswordPlugin::new(user_manager.clone(), storage.clone())
385            .register_user_with_password("test@example.com", "123", None)
386            .await;
387
388        assert!(matches!(
389            result,
390            Err(Error::Validation(ValidationError::WeakPassword))
391        ));
392
393        Ok(())
394    }
395
396    #[tokio::test]
397    async fn test_incorrect_password_login() -> Result<(), Error> {
398        let storage = setup_storage().await?;
399        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
400
401        PasswordPlugin::new(user_manager.clone(), storage.clone())
402            .register_user_with_password("test@example.com", "password", Some(Utc::now()))
403            .await?;
404
405        let result = PasswordPlugin::new(user_manager.clone(), storage.clone())
406            .login_user_with_password("test@example.com", "wrong-password")
407            .await;
408
409        assert!(matches!(
410            result,
411            Err(Error::Auth(AuthError::InvalidCredentials))
412        ));
413
414        Ok(())
415    }
416
417    #[tokio::test]
418    async fn test_nonexistent_user_login() -> Result<(), Error> {
419        let storage = setup_storage().await?;
420        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
421
422        let result = PasswordPlugin::new(user_manager.clone(), storage.clone())
423            .login_user_with_password("nonexistent@example.com", "password")
424            .await;
425
426        assert!(matches!(result, Err(Error::Auth(AuthError::UserNotFound))));
427
428        Ok(())
429    }
430
431    #[tokio::test]
432    async fn test_sql_injection_attempt() -> Result<(), Error> {
433        let storage = setup_storage().await?;
434        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
435
436        let result = PasswordPlugin::new(user_manager.clone(), storage.clone())
437            .register_user_with_password("test@example.com'; DROP TABLE users;--", "password", None)
438            .await;
439
440        assert!(matches!(
441            result,
442            Err(Error::Validation(ValidationError::InvalidEmail))
443        ));
444
445        Ok(())
446    }
447
448    #[tokio::test]
449    async fn test_change_password() -> Result<(), Error> {
450        let storage = setup_storage().await?;
451        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
452
453        let plugin = PasswordPlugin::new(user_manager.clone(), storage.clone());
454
455        // Create initial user
456        let user = plugin
457            .register_user_with_password("test@example.com", "password", Some(Utc::now()))
458            .await?;
459
460        // Change password
461        plugin
462            .change_user_password(&user.id, "password", "new-password")
463            .await?;
464
465        // Verify can login with new password
466        let result = plugin
467            .login_user_with_password("test@example.com", "new-password")
468            .await;
469        assert!(result.is_ok());
470
471        // Verify can't login with old password
472        let result = plugin
473            .login_user_with_password("test@example.com", "password")
474            .await;
475        assert!(matches!(
476            result,
477            Err(Error::Auth(AuthError::InvalidCredentials))
478        ));
479
480        Ok(())
481    }
482
483    #[tokio::test]
484    async fn test_event_handler_emitting() -> Result<(), Error> {
485        let _ = tracing_subscriber::fmt().try_init();
486
487        let storage = setup_storage().await?;
488        let user_manager = Arc::new(DefaultUserManager::new(storage.clone()));
489
490        let event_bus = EventBus::new();
491        let event_was_emitted = Arc::new(AtomicBool::new(false));
492        let event_count = Arc::new(AtomicUsize::new(0));
493
494        let handler = TestEventHandler {
495            called: event_was_emitted.clone(),
496            call_count: event_count.clone(),
497        };
498        event_bus.register(Arc::new(handler)).await;
499
500        let plugin =
501            PasswordPlugin::new(user_manager.clone(), storage.clone()).with_event_bus(event_bus);
502
503        // Test user creation event
504        let user = plugin
505            .register_user_with_password("test@example.com", "password", Some(Utc::now()))
506            .await?;
507
508        assert!(
509            event_was_emitted.load(Ordering::SeqCst),
510            "Expected event to be emitted on user creation"
511        );
512        assert_eq!(event_count.load(Ordering::SeqCst), 1);
513
514        // Test password change event
515        event_was_emitted.store(false, Ordering::SeqCst);
516        plugin
517            .change_user_password(&user.id, "password", "new-password")
518            .await?;
519
520        assert!(
521            event_was_emitted.load(Ordering::SeqCst),
522            "Expected event to be emitted on password change"
523        );
524        assert_eq!(event_count.load(Ordering::SeqCst), 2);
525
526        Ok(())
527    }
528
529    struct TestEventHandler {
530        called: Arc<AtomicBool>,
531        call_count: Arc<AtomicUsize>,
532    }
533
534    #[async_trait]
535    impl EventHandler for TestEventHandler {
536        async fn handle_event(&self, _event: &Event) -> Result<(), EventError> {
537            self.called.store(true, Ordering::SeqCst);
538            self.call_count.fetch_add(1, Ordering::SeqCst);
539            Ok(())
540        }
541    }
542}