arcly_http_identity/mfa/mod.rs
1//! Multi-factor authentication: the [`MfaVerifier`] hook the login flow calls,
2//! plus concrete factors (TOTP today, passkeys behind the `passkey` feature).
3
4use async_trait::async_trait;
5
6use crate::error::Result;
7
8#[cfg(feature = "totp")]
9pub mod totp;
10
11#[cfg(feature = "passkey")]
12pub mod passkey;
13
14/// The seam between [`IdentityService`](crate::identity::IdentityService) and the
15/// concrete second factors. An implementation resolves the enrolled secret for
16/// `user_id` and checks `code` for the given `method`.
17///
18/// A [`MfaRouter`] is provided to fan out to multiple factors (e.g. TOTP +
19/// recovery codes) by `method` string.
20#[async_trait]
21pub trait MfaVerifier: Send + Sync + 'static {
22 /// Verify a factor response. `method` is `"totp"`, `"recovery"`, `"passkey"`,
23 /// etc. Returns `Ok(true)` on success, `Ok(false)` on a wrong-but-well-formed
24 /// code, and `Err` only on backend failure.
25 async fn verify(&self, user_id: &str, method: &str, code: &str) -> Result<bool>;
26}
27
28/// Dispatches an MFA verification to the first registered factor that claims the
29/// `method`. Lets an app enrol several factors behind one `MfaVerifier`.
30#[derive(Default)]
31pub struct MfaRouter {
32 factors: Vec<(String, std::sync::Arc<dyn MfaVerifier>)>,
33}
34
35impl MfaRouter {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 /// Register `verifier` as the handler for `method`.
41 pub fn register(mut self, method: &str, verifier: std::sync::Arc<dyn MfaVerifier>) -> Self {
42 self.factors.push((method.to_owned(), verifier));
43 self
44 }
45}
46
47#[async_trait]
48impl MfaVerifier for MfaRouter {
49 async fn verify(&self, user_id: &str, method: &str, code: &str) -> Result<bool> {
50 for (m, v) in &self.factors {
51 if m == method {
52 return v.verify(user_id, method, code).await;
53 }
54 }
55 // Unknown method → not verified (never an error the caller can probe).
56 Ok(false)
57 }
58}