Skip to main content

alibi_api/plugins/two_factor/
endpoint.rs

1use super::TwoFactorPlugin;
2use crate::plugins::authentication_helpers::JsonField;
3use crate::plugins::endpoint::{definition, validate_fields};
4use alibi_core::endpoint::{
5    EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
6};
7use alibi_core::utils::json::JsValue;
8use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
9use serde::Deserialize;
10
11#[derive(Debug, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct BackupCodesOutput {
14    pub status: bool,
15    pub backup_codes: serde_json::Value,
16}
17
18#[derive(Debug, Deserialize)]
19pub struct TotpOutput {
20    pub code: String,
21}
22
23pub(super) fn definitions() -> Vec<EndpointDefinition> {
24    vec![
25        definition("viewBackupCodes", "viewBackupCodes", None, HttpMethod::Post),
26        definition("generateTOTP", "generateTOTP", None, HttpMethod::Post),
27    ]
28}
29
30pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
31    let field = if call.operation_id() == "viewBackupCodes" {
32        "userId"
33    } else {
34        "secret"
35    };
36    let mut body = call.body().cloned();
37    if field == "userId"
38        && let Some(JsValue::Object(body)) = &mut body
39    {
40        let user_id = body
41            .get(field)
42            .map_or_else(|| Ok("undefined".into()), JsValue::coerce_string)
43            .map_err(crate::plugins::endpoint::validation)?;
44        drop(body.insert(field.into(), JsValue::String(user_id)));
45    }
46    Ok(EndpointInput {
47        body: Some(validate_fields(
48            body.as_ref(),
49            "body",
50            &[JsonField::string(field, true)],
51        )?),
52        query: call.query().cloned(),
53    })
54}
55
56impl TwoFactorPlugin {
57    /// Read trusted server-only backup codes through installed endpoint hooks.
58    #[must_use]
59    pub fn view_backup_codes_endpoint(
60        user_id: impl Into<String>,
61    ) -> ServerEndpoint<BackupCodesOutput> {
62        ServerEndpoint::new("two-factor", "viewBackupCodes")
63            .with_body_value(single_input("userId", user_id.into()))
64    }
65
66    /// Generate a TOTP through installed endpoint hooks and current configured policy.
67    #[must_use]
68    pub fn generate_totp_endpoint(secret: impl Into<String>) -> ServerEndpoint<TotpOutput> {
69        ServerEndpoint::new("two-factor", "generateTOTP")
70            .with_body_value(single_input("secret", secret.into()))
71    }
72
73    pub(super) async fn call_endpoint(
74        &self,
75        call: &EndpointCall,
76        ctx: &AuthContext<impl AuthSchema>,
77    ) -> AuthResult<EndpointResponse> {
78        if call.operation_id() == "viewBackupCodes" {
79            #[derive(Deserialize)]
80            #[serde(rename_all = "camelCase")]
81            struct Body {
82                user_id: String,
83            }
84            let body: Body = call.body_as()?;
85            EndpointResponse::json(
86                &serde_json::json!({"status":true,"backupCodes":self.view_backup_codes(&body.user_id,ctx).await?}),
87            )
88        } else {
89            #[derive(Deserialize)]
90            struct Body {
91                secret: String,
92            }
93            let body: Body = call.body_as()?;
94            EndpointResponse::json(&serde_json::json!({"code":self.generate_totp(&body.secret)?}))
95        }
96    }
97}
98
99fn single_input(name: &str, value: String) -> JsValue {
100    JsValue::Object(
101        [(name.into(), JsValue::String(value))]
102            .into_iter()
103            .collect(),
104    )
105}