Skip to main content

better_auth_api/plugins/two_factor/
native.rs

1use super::{TwoFactorConfig, TwoFactorPlugin, request};
2use better_auth_core::{
3    AuthContext, AuthError, AuthResponse, AuthResult, AuthRoute, AuthSchema, HttpMethod,
4    NativeRequest,
5};
6use serde_json::Value;
7
8/// Server-only Two Factor endpoints bound to this instance's registered options.
9/// These operations have no public HTTP routes.
10pub struct TwoFactorApi<'a, S: AuthSchema> {
11    plugin: TwoFactorPlugin,
12    context: &'a AuthContext<S>,
13    source: NativeRequest<'a>,
14}
15impl<'a, S: AuthSchema> TwoFactorApi<'a, S> {
16    /// Bind to the plugin configuration installed on this auth instance.
17    pub fn from_context(context: &'a AuthContext<S>) -> AuthResult<Self> {
18        let config = context
19            .extensions
20            .get::<TwoFactorConfig>()
21            .ok_or_else(|| AuthError::config("TwoFactorPlugin is not registered"))?;
22        Ok(Self {
23            plugin: TwoFactorPlugin::with_config(config.clone()),
24            context,
25            source: NativeRequest::default(),
26        })
27    }
28    /// Preserve an explicit native Request for dynamic runtime resolution.
29    pub fn with_request(mut self, source: NativeRequest<'a>) -> Self {
30        self.source = source;
31        self
32    }
33    /// Validate a native body and generate a TOTP using the registered digit and period options.
34    pub async fn generate_totp(&self, body: Option<Value>) -> AuthResult<String> {
35        let route = AuthRoute::server_only(HttpMethod::Post, "generateTOTP")
36            .body_validator(|request| request::validate_native(request, "generateTOTP"));
37        let response = self
38            .context
39            .dispatch_native(self.source, route, body, None, |request, _| async move {
40                let body = request
41                    .validated_body::<request::GenerateTotpRequest>()
42                    .ok_or_else(|| AuthError::internal("Missing validated TOTP input"))?;
43                AuthResponse::json(
44                    200,
45                    &serde_json::json!({"code":self.plugin.generate_totp(&body.secret)?}),
46                )
47                .map_err(Into::into)
48            })
49            .await?;
50        #[derive(serde::Deserialize)]
51        struct Result {
52            code: String,
53        }
54        Ok(serde_json::from_slice::<Result>(&response.body.bytes()?)?.code)
55    }
56    /// Validate and coerce the supplied user ID, then read the stored backup codes.
57    pub async fn view_backup_codes(&self, body: Option<Value>) -> AuthResult<Vec<String>> {
58        let route = AuthRoute::server_only(HttpMethod::Post, "viewBackupCodes")
59            .body_validator(|request| request::validate_native(request, "viewBackupCodes"));
60        let response = self
61            .context
62            .dispatch_native(
63                self.source,
64                route,
65                body,
66                None,
67                |request, context| async move {
68                    let body = request
69                        .validated_body::<request::ViewBackupCodesRequest>()
70                        .ok_or_else(|| {
71                            AuthError::internal("Missing validated backup code input")
72                        })?;
73                    let codes = self
74                        .plugin
75                        .view_backup_codes(&body.user_id, &context)
76                        .await?;
77                    AuthResponse::json(200, &serde_json::json!({"status":true,"backupCodes":codes}))
78                        .map_err(Into::into)
79                },
80            )
81            .await?;
82        #[derive(serde::Deserialize)]
83        struct Result {
84            #[serde(rename = "backupCodes")]
85            backup_codes: Vec<String>,
86        }
87        Ok(serde_json::from_slice::<Result>(&response.body.bytes()?)?.backup_codes)
88    }
89}