torii_core/services/
passkey.rs

1use crate::{
2    Error, User, UserId,
3    repositories::{PasskeyCredential, PasskeyRepository, UserRepository},
4    services::UserService,
5};
6use std::sync::Arc;
7
8/// Service for passkey/WebAuthn authentication operations
9pub struct PasskeyService<U: UserRepository, P: PasskeyRepository> {
10    user_service: Arc<UserService<U>>,
11    passkey_repository: Arc<P>,
12}
13
14impl<U: UserRepository, P: PasskeyRepository> PasskeyService<U, P> {
15    /// Create a new PasskeyService with the given repositories
16    pub fn new(user_repository: Arc<U>, passkey_repository: Arc<P>) -> Self {
17        let user_service = Arc::new(UserService::new(user_repository));
18        Self {
19            user_service,
20            passkey_repository,
21        }
22    }
23
24    /// Register a new passkey credential for a user
25    pub async fn register_credential(
26        &self,
27        user_id: &UserId,
28        credential_id: Vec<u8>,
29        public_key: Vec<u8>,
30        name: Option<String>,
31    ) -> Result<PasskeyCredential, Error> {
32        self.passkey_repository
33            .add_credential(user_id, credential_id, public_key, name)
34            .await
35    }
36
37    /// Get all passkey credentials for a user
38    pub async fn get_user_credentials(
39        &self,
40        user_id: &UserId,
41    ) -> Result<Vec<PasskeyCredential>, Error> {
42        self.passkey_repository
43            .get_credentials_for_user(user_id)
44            .await
45    }
46
47    /// Get a specific passkey credential
48    pub async fn get_credential(
49        &self,
50        credential_id: &[u8],
51    ) -> Result<Option<PasskeyCredential>, Error> {
52        self.passkey_repository.get_credential(credential_id).await
53    }
54
55    /// Authenticate with a passkey credential
56    pub async fn authenticate_credential(
57        &self,
58        credential_id: &[u8],
59    ) -> Result<Option<User>, Error> {
60        // Get the credential
61        let credential = self
62            .passkey_repository
63            .get_credential(credential_id)
64            .await?;
65
66        if let Some(cred) = credential {
67            // Update last used timestamp
68            self.passkey_repository
69                .update_last_used(credential_id)
70                .await?;
71
72            // Get the user
73            let user = self.user_service.get_user(&cred.user_id).await?;
74
75            Ok(user)
76        } else {
77            Ok(None)
78        }
79    }
80
81    /// Delete a passkey credential
82    pub async fn delete_credential(&self, credential_id: &[u8]) -> Result<(), Error> {
83        self.passkey_repository
84            .delete_credential(credential_id)
85            .await
86    }
87
88    /// Delete all passkey credentials for a user
89    pub async fn delete_user_credentials(&self, user_id: &UserId) -> Result<(), Error> {
90        self.passkey_repository.delete_all_for_user(user_id).await
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::repositories::{PasskeyCredential, PasskeyRepository, UserRepository};
98    use crate::{User, UserId};
99    use async_trait::async_trait;
100    use chrono::{DateTime, Utc};
101    use std::collections::HashMap;
102    use std::sync::Arc;
103    use tokio::sync::Mutex;
104
105    // Mock implementations for testing
106    #[derive(Debug, Clone)]
107    struct MockUser {
108        id: UserId,
109        email: String,
110        name: Option<String>,
111        created_at: DateTime<Utc>,
112        updated_at: DateTime<Utc>,
113    }
114
115    impl From<MockUser> for User {
116        fn from(user: MockUser) -> Self {
117            User {
118                id: user.id,
119                email: user.email,
120                name: user.name,
121                email_verified_at: None,
122                created_at: user.created_at,
123                updated_at: user.updated_at,
124            }
125        }
126    }
127
128    #[derive(Default)]
129    struct MockUserRepository {
130        users: Arc<Mutex<HashMap<UserId, MockUser>>>,
131    }
132
133    #[async_trait]
134    impl UserRepository for MockUserRepository {
135        async fn create(&self, new_user: crate::storage::NewUser) -> Result<User, Error> {
136            let user = MockUser {
137                id: UserId::new_random(),
138                email: new_user.email,
139                name: new_user.name,
140                created_at: Utc::now(),
141                updated_at: Utc::now(),
142            };
143
144            self.users
145                .lock()
146                .await
147                .insert(user.id.clone(), user.clone());
148            Ok(user.into())
149        }
150
151        async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, Error> {
152            Ok(self.users.lock().await.get(id).cloned().map(Into::into))
153        }
154
155        async fn find_by_email(&self, email: &str) -> Result<Option<User>, Error> {
156            Ok(self
157                .users
158                .lock()
159                .await
160                .values()
161                .find(|u| u.email == email)
162                .cloned()
163                .map(Into::into))
164        }
165
166        async fn find_or_create_by_email(&self, email: &str) -> Result<User, Error> {
167            if let Some(user) = self.find_by_email(email).await? {
168                Ok(user)
169            } else {
170                let new_user = crate::storage::NewUser::builder()
171                    .email(email.to_string())
172                    .build()
173                    .unwrap();
174                self.create(new_user).await
175            }
176        }
177
178        async fn update(&self, _user: &User) -> Result<User, Error> {
179            unimplemented!()
180        }
181
182        async fn delete(&self, _id: &UserId) -> Result<(), Error> {
183            unimplemented!()
184        }
185
186        async fn mark_email_verified(&self, _user_id: &UserId) -> Result<(), Error> {
187            Ok(())
188        }
189    }
190
191    #[derive(Default)]
192    struct MockPasskeyRepository {
193        credentials: Arc<Mutex<HashMap<Vec<u8>, PasskeyCredential>>>,
194        user_credentials: Arc<Mutex<HashMap<UserId, Vec<Vec<u8>>>>>,
195    }
196
197    #[async_trait]
198    impl PasskeyRepository for MockPasskeyRepository {
199        async fn add_credential(
200            &self,
201            user_id: &UserId,
202            credential_id: Vec<u8>,
203            public_key: Vec<u8>,
204            name: Option<String>,
205        ) -> Result<PasskeyCredential, Error> {
206            let credential = PasskeyCredential {
207                credential_id: credential_id.clone(),
208                user_id: user_id.clone(),
209                public_key,
210                name,
211                created_at: Utc::now(),
212                last_used_at: None,
213            };
214
215            self.credentials
216                .lock()
217                .await
218                .insert(credential_id.clone(), credential.clone());
219            self.user_credentials
220                .lock()
221                .await
222                .entry(user_id.clone())
223                .or_insert_with(Vec::new)
224                .push(credential_id);
225
226            Ok(credential)
227        }
228
229        async fn get_credential(
230            &self,
231            credential_id: &[u8],
232        ) -> Result<Option<PasskeyCredential>, Error> {
233            Ok(self.credentials.lock().await.get(credential_id).cloned())
234        }
235
236        async fn get_credentials_for_user(
237            &self,
238            user_id: &UserId,
239        ) -> Result<Vec<PasskeyCredential>, Error> {
240            let credentials = self.credentials.lock().await;
241            let user_creds = self.user_credentials.lock().await;
242
243            if let Some(cred_ids) = user_creds.get(user_id) {
244                let mut result = Vec::new();
245                for cred_id in cred_ids {
246                    if let Some(cred) = credentials.get(cred_id) {
247                        result.push(cred.clone());
248                    }
249                }
250                Ok(result)
251            } else {
252                Ok(Vec::new())
253            }
254        }
255
256        async fn update_last_used(&self, credential_id: &[u8]) -> Result<(), Error> {
257            if let Some(cred) = self.credentials.lock().await.get_mut(credential_id) {
258                cred.last_used_at = Some(Utc::now());
259            }
260            Ok(())
261        }
262
263        async fn delete_credential(&self, credential_id: &[u8]) -> Result<(), Error> {
264            self.credentials.lock().await.remove(credential_id);
265            // Also remove from user credentials
266            let mut user_creds = self.user_credentials.lock().await;
267            for (_, cred_ids) in user_creds.iter_mut() {
268                cred_ids.retain(|id| id != credential_id);
269            }
270            Ok(())
271        }
272
273        async fn delete_all_for_user(&self, user_id: &UserId) -> Result<(), Error> {
274            let mut user_creds = self.user_credentials.lock().await;
275            if let Some(cred_ids) = user_creds.remove(user_id) {
276                let mut credentials = self.credentials.lock().await;
277                for cred_id in cred_ids {
278                    credentials.remove(&cred_id);
279                }
280            }
281            Ok(())
282        }
283    }
284
285    #[tokio::test]
286    async fn test_register_credential() {
287        let user_repo = Arc::new(MockUserRepository::default());
288        let passkey_repo = Arc::new(MockPasskeyRepository::default());
289        let service = PasskeyService::new(user_repo, passkey_repo);
290
291        let user_id = UserId::new_random();
292        let credential_id = vec![1, 2, 3, 4];
293        let public_key = vec![5, 6, 7, 8];
294        let name = Some("My Passkey".to_string());
295
296        let result = service
297            .register_credential(
298                &user_id,
299                credential_id.clone(),
300                public_key.clone(),
301                name.clone(),
302            )
303            .await;
304        assert!(result.is_ok());
305
306        let credential = result.unwrap();
307        assert_eq!(credential.credential_id, credential_id);
308        assert_eq!(credential.user_id, user_id);
309        assert_eq!(credential.public_key, public_key);
310        assert_eq!(credential.name, name);
311    }
312
313    #[tokio::test]
314    async fn test_get_user_credentials() {
315        let user_repo = Arc::new(MockUserRepository::default());
316        let passkey_repo = Arc::new(MockPasskeyRepository::default());
317        let service = PasskeyService::new(user_repo, passkey_repo);
318
319        let user_id = UserId::new_random();
320        let credential_id = vec![1, 2, 3, 4];
321        let public_key = vec![5, 6, 7, 8];
322
323        // First register a credential
324        service
325            .register_credential(&user_id, credential_id.clone(), public_key, None)
326            .await
327            .unwrap();
328
329        // Then get credentials for user
330        let result = service.get_user_credentials(&user_id).await;
331        assert!(result.is_ok());
332
333        let credentials = result.unwrap();
334        assert_eq!(credentials.len(), 1);
335        assert_eq!(credentials[0].credential_id, credential_id);
336    }
337
338    #[tokio::test]
339    async fn test_get_credential() {
340        let user_repo = Arc::new(MockUserRepository::default());
341        let passkey_repo = Arc::new(MockPasskeyRepository::default());
342        let service = PasskeyService::new(user_repo, passkey_repo);
343
344        let user_id = UserId::new_random();
345        let credential_id = vec![1, 2, 3, 4];
346        let public_key = vec![5, 6, 7, 8];
347
348        // First register a credential
349        service
350            .register_credential(&user_id, credential_id.clone(), public_key, None)
351            .await
352            .unwrap();
353
354        // Then get the specific credential
355        let result = service.get_credential(&credential_id).await;
356        assert!(result.is_ok());
357
358        let credential = result.unwrap();
359        assert!(credential.is_some());
360        assert_eq!(credential.unwrap().credential_id, credential_id);
361    }
362
363    #[tokio::test]
364    async fn test_get_credential_not_found() {
365        let user_repo = Arc::new(MockUserRepository::default());
366        let passkey_repo = Arc::new(MockPasskeyRepository::default());
367        let service = PasskeyService::new(user_repo, passkey_repo);
368
369        let result = service.get_credential(&[9, 9, 9]).await;
370        assert!(result.is_ok());
371        assert!(result.unwrap().is_none());
372    }
373
374    #[tokio::test]
375    async fn test_authenticate_credential() {
376        let user_repo = Arc::new(MockUserRepository::default());
377        let passkey_repo = Arc::new(MockPasskeyRepository::default());
378        let service = PasskeyService::new(user_repo.clone(), passkey_repo);
379
380        let credential_id = vec![1, 2, 3, 4];
381        let public_key = vec![5, 6, 7, 8];
382
383        // First create a user and register a credential
384        let new_user = crate::storage::NewUser::builder()
385            .email("test@example.com".to_string())
386            .build()
387            .unwrap();
388        let user = user_repo.create(new_user).await.unwrap();
389        service
390            .register_credential(&user.id, credential_id.clone(), public_key, None)
391            .await
392            .unwrap();
393
394        // Then authenticate with the credential
395        let result = service.authenticate_credential(&credential_id).await;
396        assert!(result.is_ok());
397
398        let auth_user = result.unwrap();
399        assert!(auth_user.is_some());
400        assert_eq!(auth_user.unwrap().email, "test@example.com");
401    }
402
403    #[tokio::test]
404    async fn test_authenticate_credential_not_found() {
405        let user_repo = Arc::new(MockUserRepository::default());
406        let passkey_repo = Arc::new(MockPasskeyRepository::default());
407        let service = PasskeyService::new(user_repo, passkey_repo);
408
409        let result = service.authenticate_credential(&[9, 9, 9]).await;
410        assert!(result.is_ok());
411        assert!(result.unwrap().is_none());
412    }
413
414    #[tokio::test]
415    async fn test_delete_credential() {
416        let user_repo = Arc::new(MockUserRepository::default());
417        let passkey_repo = Arc::new(MockPasskeyRepository::default());
418        let service = PasskeyService::new(user_repo, passkey_repo);
419
420        let user_id = UserId::new_random();
421        let credential_id = vec![1, 2, 3, 4];
422        let public_key = vec![5, 6, 7, 8];
423
424        // First register a credential
425        service
426            .register_credential(&user_id, credential_id.clone(), public_key, None)
427            .await
428            .unwrap();
429
430        // Verify it exists
431        let result = service.get_credential(&credential_id).await;
432        assert!(result.is_ok());
433        assert!(result.unwrap().is_some());
434
435        // Delete it
436        let result = service.delete_credential(&credential_id).await;
437        assert!(result.is_ok());
438
439        // Verify it's gone
440        let result = service.get_credential(&credential_id).await;
441        assert!(result.is_ok());
442        assert!(result.unwrap().is_none());
443    }
444
445    #[tokio::test]
446    async fn test_delete_user_credentials() {
447        let user_repo = Arc::new(MockUserRepository::default());
448        let passkey_repo = Arc::new(MockPasskeyRepository::default());
449        let service = PasskeyService::new(user_repo, passkey_repo);
450
451        let user_id = UserId::new_random();
452        let credential_id1 = vec![1, 2, 3, 4];
453        let credential_id2 = vec![5, 6, 7, 8];
454        let public_key = vec![9, 10, 11, 12];
455
456        // Register two credentials for the user
457        service
458            .register_credential(&user_id, credential_id1.clone(), public_key.clone(), None)
459            .await
460            .unwrap();
461        service
462            .register_credential(&user_id, credential_id2.clone(), public_key, None)
463            .await
464            .unwrap();
465
466        // Verify both exist
467        let result = service.get_user_credentials(&user_id).await;
468        assert!(result.is_ok());
469        assert_eq!(result.unwrap().len(), 2);
470
471        // Delete all credentials for the user
472        let result = service.delete_user_credentials(&user_id).await;
473        assert!(result.is_ok());
474
475        // Verify they're gone
476        let result = service.get_user_credentials(&user_id).await;
477        assert!(result.is_ok());
478        assert_eq!(result.unwrap().len(), 0);
479    }
480}