Skip to main content

authx_plugins/totp/
mod.rs

1mod 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        // Persist the credential with hashed backup codes directly.
88        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}