authx_plugins/totp/
mod.rs1mod service;
2
3pub use service::{TotpService, TotpSetup, TotpVerifyRequest};
4
5#[cfg(test)]
6mod tests {
7 use super::service::{TotpService, TotpVerifyRequest};
8 use authx_core::models::CreateUser;
9 use authx_storage::memory::MemoryStore;
10 use authx_storage::ports::UserRepository;
11
12 fn store() -> MemoryStore {
13 MemoryStore::new()
14 }
15
16 async fn make_user(store: &MemoryStore) -> uuid::Uuid {
17 UserRepository::create(
18 store,
19 CreateUser {
20 email: "totp@example.com".into(),
21 username: None,
22 metadata: None,
23 },
24 )
25 .await
26 .unwrap()
27 .id
28 }
29
30 #[tokio::test]
31 async fn begin_setup_returns_secret_and_uri() {
32 let store = store();
33 let svc = TotpService::new(store.clone(), "TestApp");
34 let uid = make_user(&store).await;
35
36 let setup = svc.begin_setup(uid).await.unwrap();
37 assert!(!setup.secret_base32.is_empty());
38 assert!(setup.otpauth_uri.starts_with("otpauth://totp/"));
39 assert_eq!(setup.backup_codes.len(), 8);
40 }
41
42 #[tokio::test]
43 async fn begin_setup_fails_for_unknown_user() {
44 let svc = TotpService::new(store(), "TestApp");
45 let err = svc.begin_setup(uuid::Uuid::new_v4()).await.unwrap_err();
46 assert!(matches!(err, authx_core::error::AuthError::UserNotFound));
47 }
48
49 #[tokio::test]
50 async fn is_enabled_false_before_setup() {
51 let store = store();
52 let svc = TotpService::new(store.clone(), "TestApp");
53 let uid = make_user(&store).await;
54 assert!(!svc.is_enabled(uid).await.unwrap());
55 }
56
57 #[tokio::test]
58 async fn verify_fails_with_bad_code_when_not_enrolled() {
59 let store = store();
60 let svc = TotpService::new(store.clone(), "TestApp");
61 let uid = make_user(&store).await;
62
63 let err = svc
64 .verify(TotpVerifyRequest {
65 user_id: uid,
66 code: "000000".into(),
67 })
68 .await
69 .unwrap_err();
70 assert!(matches!(err, authx_core::error::AuthError::InvalidToken));
71 }
72
73 #[tokio::test]
74 async fn backup_code_accepted_after_enroll() {
75 use authx_core::crypto::sha256_hex;
76 use authx_core::models::{CreateCredential, CredentialKind};
77 use authx_storage::ports::CredentialRepository;
78
79 let store = store();
80 let svc = TotpService::new(store.clone(), "TestApp");
81 let uid = make_user(&store).await;
82
83 let setup = svc.begin_setup(uid).await.unwrap();
84 let raw_code = setup.backup_codes[0].clone();
85 let hash = sha256_hex(raw_code.as_bytes());
86
87 CredentialRepository::create(
89 &store,
90 CreateCredential {
91 user_id: uid,
92 kind: CredentialKind::Passkey,
93 credential_hash: setup.secret_base32.clone(),
94 metadata: Some(serde_json::json!({ "backup_codes": [hash] })),
95 },
96 )
97 .await
98 .unwrap();
99
100 svc.verify(TotpVerifyRequest {
101 user_id: uid,
102 code: raw_code,
103 })
104 .await
105 .unwrap();
106 }
107}