alibi_plugins/two_factor/
endpoint.rs1use super::TwoFactorPlugin;
2use crate::authentication_helpers::JsonField;
3use crate::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::endpoint::validation)?;
44 _ = 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 #[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 #[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}