arcly-http-identity 0.8.0

CIAM building-block engine for arcly-http: identity store traits, password hashing, MFA (TOTP), passwordless & account lifecycle, risk signals, and an OIDC provider — storage-agnostic, zero-lock, all I/O behind traits.
Documentation
//! Multi-factor authentication: the [`MfaVerifier`] hook the login flow calls,
//! plus concrete factors (TOTP today, passkeys behind the `passkey` feature).

use async_trait::async_trait;

use crate::error::Result;

#[cfg(feature = "totp")]
pub mod totp;

#[cfg(feature = "passkey")]
pub mod passkey;

/// The seam between [`IdentityService`](crate::identity::IdentityService) and the
/// concrete second factors. An implementation resolves the enrolled secret for
/// `user_id` and checks `code` for the given `method`.
///
/// A [`MfaRouter`] is provided to fan out to multiple factors (e.g. TOTP +
/// recovery codes) by `method` string.
#[async_trait]
pub trait MfaVerifier: Send + Sync + 'static {
    /// Verify a factor response. `method` is `"totp"`, `"recovery"`, `"passkey"`,
    /// etc. Returns `Ok(true)` on success, `Ok(false)` on a wrong-but-well-formed
    /// code, and `Err` only on backend failure.
    async fn verify(&self, user_id: &str, method: &str, code: &str) -> Result<bool>;
}

/// Dispatches an MFA verification to the first registered factor that claims the
/// `method`. Lets an app enrol several factors behind one `MfaVerifier`.
#[derive(Default)]
pub struct MfaRouter {
    factors: Vec<(String, std::sync::Arc<dyn MfaVerifier>)>,
}

impl MfaRouter {
    pub fn new() -> Self {
        Self::default()
    }

    /// Register `verifier` as the handler for `method`.
    pub fn register(mut self, method: &str, verifier: std::sync::Arc<dyn MfaVerifier>) -> Self {
        self.factors.push((method.to_owned(), verifier));
        self
    }
}

#[async_trait]
impl MfaVerifier for MfaRouter {
    async fn verify(&self, user_id: &str, method: &str, code: &str) -> Result<bool> {
        for (m, v) in &self.factors {
            if m == method {
                return v.verify(user_id, method, code).await;
            }
        }
        // Unknown method → not verified (never an error the caller can probe).
        Ok(false)
    }
}