1use super::{
2 BackupOperationError, EnableTwoFactorInput, TwoFactorEnabled, TwoFactorMethod, TwoFactorPlugin,
3 enable_core,
4};
5use crate::authentication_helpers::JsonField;
6use crate::endpoint::{definition, validate_fields};
7use alibi_core::endpoint::{
8 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
9};
10use alibi_core::utils::json::JsValue;
11use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
12use serde::Deserialize;
13
14#[derive(Debug, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct BackupCodesOutput {
17 pub status: bool,
18 pub backup_codes: serde_json::Value,
19}
20
21#[derive(Debug, Deserialize)]
22pub struct TotpOutput {
23 pub code: String,
24}
25
26#[derive(Debug, Deserialize)]
28pub struct TwoFactorDisabled {
29 pub status: bool,
30}
31
32#[derive(Debug, Deserialize)]
34pub struct TotpVerified {
35 pub token: String,
36 pub user: serde_json::Value,
37}
38
39#[derive(Debug, Deserialize)]
41pub struct TwoFactorOtpSent {
42 pub status: bool,
43}
44
45#[derive(Debug, Deserialize)]
47pub struct BackupCodeVerified {
48 pub token: Option<String>,
49 pub user: serde_json::Value,
50}
51
52pub(super) fn definitions() -> Vec<EndpointDefinition> {
53 vec![
54 definition(
55 "generateBackupCodes",
56 "generateBackupCodes",
57 Some("/two-factor/generate-backup-codes"),
58 HttpMethod::Post,
59 ),
60 definition(
61 "verifyBackupCode",
62 "verifyBackupCode",
63 Some("/two-factor/verify-backup-code"),
64 HttpMethod::Post,
65 ),
66 definition(
67 "verifyTwoFactorOTP",
68 "verifyTwoFactorOTP",
69 Some("/two-factor/verify-otp"),
70 HttpMethod::Post,
71 ),
72 definition(
73 "sendTwoFactorOTP",
74 "sendTwoFactorOTP",
75 Some("/two-factor/send-otp"),
76 HttpMethod::Post,
77 ),
78 definition(
79 "verifyTOTP",
80 "verifyTOTP",
81 Some("/two-factor/verify-totp"),
82 HttpMethod::Post,
83 ),
84 definition(
85 "getTOTPURI",
86 "getTOTPURI",
87 Some("/two-factor/get-totp-uri"),
88 HttpMethod::Post,
89 ),
90 definition(
91 "disableTwoFactor",
92 "disableTwoFactor",
93 Some("/two-factor/disable"),
94 HttpMethod::Post,
95 ),
96 definition(
97 "enableTwoFactor",
98 "enableTwoFactor",
99 Some("/two-factor/enable"),
100 HttpMethod::Post,
101 ),
102 definition("viewBackupCodes", "viewBackupCodes", None, HttpMethod::Post),
103 definition("generateTOTP", "generateTOTP", None, HttpMethod::Post),
104 ]
105}
106
107pub(super) fn validate(
108 call: &EndpointCall,
109 config: &super::TwoFactorConfig,
110) -> AuthResult<EndpointInput> {
111 let allow_passwordless = config.allow_passwordless;
112 if call.operation_id() == "verifyBackupCode" {
113 return Ok(EndpointInput {
114 body: Some(validate_fields(
115 call.body(),
116 "body",
117 &[
118 JsonField::string("code", true),
119 JsonField {
120 name: "disableSession",
121 kind: crate::authentication_helpers::JsonFieldKind::Boolean,
122 required: false,
123 },
124 JsonField {
125 name: "trustDevice",
126 kind: crate::authentication_helpers::JsonFieldKind::Boolean,
127 required: false,
128 },
129 ],
130 )?),
131 query: call.query().cloned(),
132 });
133 }
134 if call.operation_id() == "sendTwoFactorOTP" {
135 return Ok(EndpointInput {
136 body: Some(validate_fields(
137 call.body(),
138 "body",
139 &[JsonField {
140 name: "trustDevice",
141 kind: crate::authentication_helpers::JsonFieldKind::Boolean,
142 required: false,
143 }],
144 )?),
145 query: call.query().cloned(),
146 });
147 }
148 if matches!(call.operation_id(), "verifyTOTP" | "verifyTwoFactorOTP") {
149 return Ok(EndpointInput {
150 body: Some(validate_fields(
151 call.body(),
152 "body",
153 &[
154 JsonField::string("code", true),
155 JsonField {
156 name: "trustDevice",
157 kind: crate::authentication_helpers::JsonFieldKind::Boolean,
158 required: false,
159 },
160 ],
161 )?),
162 query: call.query().cloned(),
163 });
164 }
165 if call.operation_id() == "generateBackupCodes" {
166 return Ok(EndpointInput {
167 body: Some(validate_fields(
168 call.body(),
169 "body",
170 &[JsonField::string(
171 "password",
172 !config
173 .backup_allow_passwordless
174 .unwrap_or(allow_passwordless),
175 )],
176 )?),
177 query: call.query().cloned(),
178 });
179 }
180 if call.operation_id() == "getTOTPURI" {
181 return Ok(EndpointInput {
182 body: Some(validate_fields(
183 call.body(),
184 "body",
185 &[JsonField::string(
186 "password",
187 !config.totp_allow_passwordless.unwrap_or(allow_passwordless),
188 )],
189 )?),
190 query: call.query().cloned(),
191 });
192 }
193 if call.operation_id() == "disableTwoFactor" {
194 return Ok(EndpointInput {
195 body: Some(validate_fields(
196 call.body(),
197 "body",
198 &[JsonField::string("password", !allow_passwordless)],
199 )?),
200 query: call.query().cloned(),
201 });
202 }
203 if call.operation_id() == "enableTwoFactor" {
204 let mut body = validate_fields(
205 call.body(),
206 "body",
207 &[
208 JsonField::string("password", !allow_passwordless),
209 JsonField {
210 name: "method",
211 kind: crate::authentication_helpers::JsonFieldKind::OneOf(&["otp", "totp"]),
212 required: false,
213 },
214 JsonField::string("issuer", false),
215 ],
216 )?;
217 if let JsValue::Object(object) = &mut body {
218 _ = object
219 .entry("method".into())
220 .or_insert_with(|| JsValue::String("totp".into()));
221 }
222 return Ok(EndpointInput {
223 body: Some(body),
224 query: call.query().cloned(),
225 });
226 }
227 let field = if call.operation_id() == "viewBackupCodes" {
228 "userId"
229 } else {
230 "secret"
231 };
232 let mut body = call.body().cloned();
233 if field == "userId"
234 && let Some(JsValue::Object(body)) = &mut body
235 {
236 let user_id = body
237 .get(field)
238 .map_or_else(|| Ok("undefined".into()), JsValue::coerce_string)
239 .map_err(crate::endpoint::validation)?;
240 _ = body.insert(field.into(), JsValue::String(user_id));
241 }
242 Ok(EndpointInput {
243 body: Some(validate_fields(
244 body.as_ref(),
245 "body",
246 &[JsonField::string(field, true)],
247 )?),
248 query: call.query().cloned(),
249 })
250}
251
252fn verification_response(output: alibi_core::AuthResponse) -> AuthResult<EndpointResponse> {
253 let body = alibi_core::utils::json::from_slice::<JsValue>(&output.body).ok();
254 let mut response = if output.status >= 400 {
255 let response = EndpointResponse::error(crate::endpoint::error_response(output.clone()));
256 if let Some(body) = body {
257 response.with_error_body(body)
258 } else {
259 response.without_error_body()
260 }
261 } else {
262 EndpointResponse::json(&body)?
263 };
264 response.merge_headers(output.headers);
265 Ok(response)
266}
267
268impl TwoFactorPlugin {
269 #[must_use]
271 pub fn generate_backup_codes_endpoint(
272 input: super::GenerateBackupCodesInput,
273 ) -> ServerEndpoint<BackupCodesOutput> {
274 let mut body = indexmap::IndexMap::new();
275 if let Some(password) = input.password {
276 _ = body.insert("password".into(), JsValue::String(password));
277 }
278 ServerEndpoint::new("two-factor", "generateBackupCodes")
279 .with_body_value(JsValue::Object(body))
280 }
281
282 async fn generate_backup_endpoint_call(
283 &self,
284 call: &EndpointCall,
285 ctx: &AuthContext<impl AuthSchema>,
286 ) -> AuthResult<EndpointResponse> {
287 let (user, _) = Box::pin(crate::helpers::ordinary_session(call, ctx)).await?;
288 match Box::pin(super::generate_backup_codes_core(
289 &call.body_as()?,
290 &user,
291 &self.config,
292 ctx,
293 ))
294 .await
295 {
296 Ok(output) => EndpointResponse::json(&output),
297 Err(BackupOperationError::InvalidGeneration) => Ok(EndpointResponse::error(
298 alibi_core::AuthError::internal("Backup code generation failed"),
299 )
300 .without_error_body()),
301 Err(BackupOperationError::Auth(error)) => Err(error),
302 }
303 }
304
305 #[must_use]
307 pub fn verify_backup_code_endpoint(
308 input: super::VerifyBackupCodeInput,
309 ) -> ServerEndpoint<BackupCodeVerified> {
310 let mut body = [("code".into(), JsValue::String(input.code))]
311 .into_iter()
312 .collect::<indexmap::IndexMap<_, _>>();
313 if let Some(disable_session) = input.disable_session {
314 _ = body.insert("disableSession".into(), JsValue::Bool(disable_session));
315 }
316 if let Some(trust_device) = input.trust_device {
317 _ = body.insert("trustDevice".into(), JsValue::Bool(trust_device));
318 }
319 ServerEndpoint::new("two-factor", "verifyBackupCode").with_body_value(JsValue::Object(body))
320 }
321
322 async fn verify_backup_endpoint_call(
323 &self,
324 call: &EndpointCall,
325 ctx: &AuthContext<impl AuthSchema>,
326 ) -> AuthResult<EndpointResponse> {
327 verification_response(self.verify_backup_code(&call.body_as()?, call, ctx).await?)
328 }
329
330 #[must_use]
332 pub fn verify_two_factor_otp_endpoint(
333 input: super::VerifyOtpInput,
334 ) -> ServerEndpoint<TotpVerified> {
335 let mut body = [("code".into(), JsValue::String(input.code))]
336 .into_iter()
337 .collect::<indexmap::IndexMap<_, _>>();
338 if let Some(trust_device) = input.trust_device {
339 _ = body.insert("trustDevice".into(), JsValue::Bool(trust_device));
340 }
341 ServerEndpoint::new("two-factor", "verifyTwoFactorOTP")
342 .with_body_value(JsValue::Object(body))
343 }
344
345 async fn verify_otp_endpoint_call(
346 &self,
347 call: &EndpointCall,
348 ctx: &AuthContext<impl AuthSchema>,
349 ) -> AuthResult<EndpointResponse> {
350 verification_response(self.verify_otp(&call.body_as()?, call, ctx).await?)
351 }
352
353 #[must_use]
355 pub fn send_two_factor_otp_endpoint() -> ServerEndpoint<TwoFactorOtpSent> {
356 ServerEndpoint::new("two-factor", "sendTwoFactorOTP")
357 .with_body_value(JsValue::Object(indexmap::IndexMap::new()))
358 }
359
360 async fn send_otp_endpoint_call(
361 &self,
362 call: &EndpointCall,
363 ctx: &AuthContext<impl AuthSchema>,
364 ) -> AuthResult<EndpointResponse> {
365 let output = self.send_otp(call, ctx).await?;
366 let mut response = if output.status >= 400 {
367 EndpointResponse::error(crate::endpoint::error_response(output.clone()))
368 .without_error_body()
369 } else {
370 EndpointResponse::json(&alibi_core::utils::json::from_slice::<JsValue>(
371 &output.body,
372 )?)?
373 };
374 response.merge_headers(output.headers);
375 Ok(response)
376 }
377
378 #[must_use]
380 pub fn verify_totp_endpoint(input: super::VerifyTotpInput) -> ServerEndpoint<TotpVerified> {
381 let mut body = [("code".into(), JsValue::String(input.code))]
382 .into_iter()
383 .collect::<indexmap::IndexMap<_, _>>();
384 if let Some(trust_device) = input.trust_device {
385 _ = body.insert("trustDevice".into(), JsValue::Bool(trust_device));
386 }
387 ServerEndpoint::new("two-factor", "verifyTOTP").with_body_value(JsValue::Object(body))
388 }
389
390 async fn verify_totp_endpoint_call(
391 &self,
392 call: &EndpointCall,
393 ctx: &AuthContext<impl AuthSchema>,
394 ) -> AuthResult<EndpointResponse> {
395 let output = self.verify_totp(&call.body_as()?, call, ctx).await?;
396 verification_response(output)
397 }
398
399 #[must_use]
401 pub fn get_totp_uri_endpoint(
402 input: super::GetTotpUriInput,
403 ) -> ServerEndpoint<super::TotpUriOutput> {
404 let body = input
405 .password
406 .map(|password| ("password".into(), JsValue::String(password)))
407 .into_iter()
408 .collect();
409 ServerEndpoint::new("two-factor", "getTOTPURI").with_body_value(JsValue::Object(body))
410 }
411
412 async fn uri_endpoint(
413 &self,
414 call: &EndpointCall,
415 ctx: &AuthContext<impl AuthSchema>,
416 ) -> AuthResult<EndpointResponse> {
417 let (user, _) = crate::helpers::ordinary_session(call, ctx).await?;
418 EndpointResponse::json(
419 &super::get_totp_uri_core(&call.body_as()?, &user, &self.config, ctx).await?,
420 )
421 }
422
423 #[must_use]
425 pub fn disable_two_factor_endpoint(
426 input: super::DisableTwoFactorInput,
427 ) -> ServerEndpoint<TwoFactorDisabled> {
428 let body = input
429 .password
430 .map(|password| ("password".into(), JsValue::String(password)))
431 .into_iter()
432 .collect();
433 ServerEndpoint::new("two-factor", "disableTwoFactor").with_body_value(JsValue::Object(body))
434 }
435
436 async fn disable_endpoint(
437 &self,
438 call: &EndpointCall,
439 ctx: &AuthContext<impl AuthSchema>,
440 ) -> AuthResult<EndpointResponse> {
441 let output = self.disable_factor(&call.body_as()?, call, ctx).await?;
442 let mut response = EndpointResponse::json(
443 &alibi_core::utils::json::from_slice::<JsValue>(&output.body)?,
444 )?;
445 response.merge_headers(output.headers);
446 Ok(response)
447 }
448
449 #[must_use]
451 pub fn enable_two_factor_endpoint(
452 input: EnableTwoFactorInput,
453 ) -> ServerEndpoint<TwoFactorEnabled> {
454 let mut body = [(
455 "method".into(),
456 JsValue::String(
457 match input.method {
458 TwoFactorMethod::Otp => "otp",
459 TwoFactorMethod::Totp => "totp",
460 }
461 .into(),
462 ),
463 )]
464 .into_iter()
465 .collect::<indexmap::IndexMap<_, _>>();
466 if let Some(password) = input.password {
467 _ = body.insert("password".into(), JsValue::String(password));
468 }
469 if let Some(issuer) = input.issuer {
470 _ = body.insert("issuer".into(), JsValue::String(issuer));
471 }
472 ServerEndpoint::new("two-factor", "enableTwoFactor").with_body_value(JsValue::Object(body))
473 }
474
475 async fn enable_endpoint(
476 &self,
477 call: &EndpointCall,
478 ctx: &AuthContext<impl AuthSchema>,
479 ) -> AuthResult<EndpointResponse> {
480 let (user, session) = crate::helpers::ordinary_session(call, ctx).await?;
481 match enable_core(&call.body_as()?, &user, &session, &self.config, ctx).await {
482 Ok((value, cookies)) => {
483 let mut response = EndpointResponse::json(&value)?;
484 for cookie in cookies {
485 response = response.with_header("set-cookie", cookie);
486 }
487 Ok(response)
488 }
489 Err(
490 BackupOperationError::Auth(alibi_core::AuthError::SessionCreationCancelled)
491 | BackupOperationError::InvalidGeneration,
492 ) => Ok(EndpointResponse::error(alibi_core::AuthError::Api {
493 status: 500,
494 code: None,
495 message: String::new(),
496 })
497 .without_error_body()),
498 Err(BackupOperationError::Auth(error)) => Err(error),
499 }
500 }
501
502 #[must_use]
504 pub fn view_backup_codes_endpoint(
505 user_id: impl Into<String>,
506 ) -> ServerEndpoint<BackupCodesOutput> {
507 ServerEndpoint::new("two-factor", "viewBackupCodes")
508 .with_body_value(single_input("userId", user_id.into()))
509 }
510
511 #[must_use]
513 pub fn generate_totp_endpoint(secret: impl Into<String>) -> ServerEndpoint<TotpOutput> {
514 ServerEndpoint::new("two-factor", "generateTOTP")
515 .with_body_value(single_input("secret", secret.into()))
516 }
517
518 pub(super) async fn call_endpoint(
519 &self,
520 call: &EndpointCall,
521 ctx: &AuthContext<impl AuthSchema>,
522 ) -> AuthResult<EndpointResponse> {
523 if call.operation_id() == "generateBackupCodes" {
524 Box::pin(self.generate_backup_endpoint_call(call, ctx)).await
525 } else if call.operation_id() == "verifyBackupCode" {
526 Box::pin(self.verify_backup_endpoint_call(call, ctx)).await
527 } else if call.operation_id() == "verifyTwoFactorOTP" {
528 Box::pin(self.verify_otp_endpoint_call(call, ctx)).await
529 } else if call.operation_id() == "sendTwoFactorOTP" {
530 Box::pin(self.send_otp_endpoint_call(call, ctx)).await
531 } else if call.operation_id() == "verifyTOTP" {
532 Box::pin(self.verify_totp_endpoint_call(call, ctx)).await
533 } else if call.operation_id() == "getTOTPURI" {
534 Box::pin(self.uri_endpoint(call, ctx)).await
535 } else if call.operation_id() == "disableTwoFactor" {
536 Box::pin(self.disable_endpoint(call, ctx)).await
537 } else if call.operation_id() == "enableTwoFactor" {
538 Box::pin(self.enable_endpoint(call, ctx)).await
539 } else if call.operation_id() == "viewBackupCodes" {
540 #[derive(Deserialize)]
541 #[serde(rename_all = "camelCase")]
542 struct Body {
543 user_id: String,
544 }
545 let body: Body = call.body_as()?;
546 EndpointResponse::json(
547 &serde_json::json!({"status":true,"backupCodes":Box::pin(self.view_backup_codes(&body.user_id,ctx)).await?}),
548 )
549 } else {
550 #[derive(Deserialize)]
551 struct Body {
552 secret: String,
553 }
554 let body: Body = call.body_as()?;
555 EndpointResponse::json(&serde_json::json!({"code":self.generate_totp(&body.secret)?}))
556 }
557 }
558}
559
560fn single_input(name: &str, value: String) -> JsValue {
561 JsValue::Object(
562 [(name.into(), JsValue::String(value))]
563 .into_iter()
564 .collect(),
565 )
566}