1use async_trait::async_trait;
2use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
3use chrono::{Duration, Utc};
4use hmac::{Hmac, Mac};
5use rand::Rng;
6use rand::distributions::Alphanumeric;
7use serde::{Deserialize, Serialize};
8use sha2::Sha256;
9use std::sync::Arc;
10use totp_rs::{Algorithm, TOTP};
11
12use better_auth_core::entity::{AuthSession, AuthTwoFactor, AuthUser};
13use better_auth_core::utils::cookie_utils::{
14 create_clear_cookie, create_session_like_cookie, related_cookie_name,
15};
16use better_auth_core::wire::UserView;
17use better_auth_core::{
18 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, CreateTwoFactor,
19 CreateVerification, RequestMeta, TwoFactor, UpdateUser,
20};
21
22use crate::plugins::helpers::{
23 SessionIssueError, delete_session_cookies, get_cookie, get_credential_password_hash,
24 issue_user_session, issue_user_session_with_lifetime,
25};
26
27use super::StatusResponse;
28mod actions;
29mod callbacks;
30mod challenge;
31pub use callbacks::TwoFactorCallbacks;
32mod helpers;
33mod native;
34mod options;
35mod request;
36pub use native::TwoFactorApi;
37pub use options::{
38 BackupCodeOptions, BackupCodeStorage, TwoFactorCipher, TwoFactorHasher, TwoFactorOtpStorage,
39};
40mod security;
41mod totp;
42use actions::*;
43use helpers::*;
44use security::*;
45
46#[cfg(test)]
47mod enrollment_tests;
48#[cfg(test)]
49mod security_tests;
50#[cfg(test)]
51mod tests;
52
53const TWO_FACTOR_COOKIE_SUFFIX: &str = "two_factor";
54const TRUST_DEVICE_COOKIE_SUFFIX: &str = "trust_device";
55const DONT_REMEMBER_COOKIE_SUFFIX: &str = "dont_remember";
56
57const METADATA_ENABLED: &str = "two_factor.enabled";
58const METADATA_TOTP_DISABLED: &str = "two_factor.totp_disabled";
59const METADATA_OTP_ENABLED: &str = "two_factor.otp_enabled";
60const DEFAULT_TWO_FACTOR_COOKIE_MAX_AGE_SECS: f64 = 600.0;
61const DEFAULT_TRUST_DEVICE_MAX_AGE_SECS: f64 = 2_592_000.0;
62const DEFAULT_TOTP_PERIOD_SECS: f64 = 30.0;
63const DEFAULT_TOTP_DIGITS: usize = 6;
64const CHALLENGE_ATTEMPT_LIMIT: usize = 5;
65
66type HmacSha256 = Hmac<Sha256>;
67
68#[async_trait]
70pub trait SendTwoFactorOtp: Send + Sync {
71 async fn send(&self, user: &UserView, otp: &str) -> AuthResult<()>;
73}
74
75#[derive(Clone)]
77pub struct TwoFactorPlugin {
78 config: TwoFactorConfig,
79}
80
81#[derive(Clone, better_auth_core::PluginConfig)]
83#[plugin(name = "TwoFactorPlugin")]
84pub struct TwoFactorConfig {
85 #[config(default = false)]
87 pub allow_passwordless: bool,
88 #[config(default = false)]
90 pub totp_disabled: bool,
91 #[config(default = None)]
93 pub totp_allow_passwordless: Option<bool>,
94 #[config(default = 6)]
96 pub otp_digits: usize,
97 #[config(default = Duration::minutes(3))]
99 pub otp_period: Duration,
100 #[config(default = 5)]
102 pub otp_allowed_attempts: usize,
103 #[config(default = TwoFactorOtpStorage::Plain)]
105 pub otp_storage: TwoFactorOtpStorage,
106 #[config(default = BackupCodeOptions::default())]
108 pub backup_code_options: BackupCodeOptions,
109 #[config(default = AccountLockout::default())]
111 pub account_lockout: AccountLockout,
112 #[config(default = None)]
114 pub issuer: Option<String>,
115 #[config(default = false)]
117 pub skip_verification_on_enable: bool,
118 #[config(default = DEFAULT_TWO_FACTOR_COOKIE_MAX_AGE_SECS)]
120 pub two_factor_cookie_max_age: f64,
121 #[config(default = DEFAULT_TRUST_DEVICE_MAX_AGE_SECS)]
123 pub trust_device_max_age: f64,
124 #[config(default = DEFAULT_TOTP_PERIOD_SECS)]
127 pub totp_period: f64,
128 #[config(default = DEFAULT_TOTP_DIGITS)]
130 pub totp_digits: usize,
131 #[config(default = None, skip)]
133 pub send_otp: Option<Arc<dyn SendTwoFactorOtp>>,
134}
135
136impl std::fmt::Debug for TwoFactorConfig {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 f.debug_struct("TwoFactorConfig")
139 .field("account_lockout", &self.account_lockout)
140 .field("issuer", &self.issuer)
141 .field(
142 "skip_verification_on_enable",
143 &self.skip_verification_on_enable,
144 )
145 .field("two_factor_cookie_max_age", &self.two_factor_cookie_max_age)
146 .field("trust_device_max_age", &self.trust_device_max_age)
147 .field("totp_period", &self.totp_period)
148 .field("totp_digits", &self.totp_digits)
149 .field("send_otp", &self.send_otp.as_ref().map(|_| "custom"))
150 .finish()
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct AccountLockout {
157 pub enabled: bool,
159 pub max_failed_attempts: i64,
161 pub duration_seconds: f64,
163}
164
165impl Default for AccountLockout {
166 fn default() -> Self {
167 Self {
168 enabled: true,
169 max_failed_attempts: 10,
170 duration_seconds: 900.0,
171 }
172 }
173}
174
175#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
176#[serde(rename_all = "lowercase")]
177enum EnrollmentMethod {
178 Otp,
179 #[default]
180 Totp,
181}
182
183#[derive(Debug, Clone, Deserialize)]
184pub(crate) struct EnableRequest {
185 password: Option<String>,
186 issuer: Option<String>,
187 #[serde(default)]
188 method: EnrollmentMethod,
189}
190
191#[derive(Debug, Clone, Deserialize)]
192pub(crate) struct DisableRequest {
193 password: Option<String>,
194}
195
196#[derive(Debug, Clone, Deserialize)]
197pub(crate) struct GetTotpUriRequest {
198 password: Option<String>,
199}
200
201#[derive(Debug, Clone, Deserialize)]
202pub(crate) struct VerifyTotpRequest {
203 code: String,
204 #[serde(rename = "trustDevice")]
205 trust_device: Option<bool>,
206}
207
208#[derive(Debug, Clone, Deserialize)]
209pub(crate) struct VerifyOtpRequest {
210 code: String,
211 #[serde(rename = "trustDevice")]
212 trust_device: Option<bool>,
213}
214
215#[derive(Debug, Clone, Deserialize)]
216pub(crate) struct GenerateBackupCodesRequest {
217 password: Option<String>,
218}
219
220#[derive(Debug, Clone, Deserialize)]
221pub(crate) struct VerifyBackupCodeRequest {
222 code: String,
223 #[serde(rename = "disableSession")]
224 disable_session: Option<bool>,
225 #[serde(rename = "trustDevice")]
226 trust_device: Option<bool>,
227}
228
229#[derive(Debug, Serialize)]
230pub(crate) struct EnableResponse {
231 method: &'static str,
232 #[serde(rename = "totpURI", skip_serializing_if = "Option::is_none")]
233 totp_uri: Option<String>,
234 #[serde(rename = "backupCodes", skip_serializing_if = "Option::is_none")]
235 backup_codes: Option<Vec<String>>,
236}
237
238#[derive(Debug, Serialize)]
239pub(crate) struct TotpUriResponse {
240 #[serde(rename = "totpURI")]
241 totp_uri: String,
242}
243
244#[derive(Debug, Serialize)]
245pub(crate) struct SessionTokenResponse<U: Serialize> {
246 #[serde(skip_serializing_if = "Option::is_none")]
247 token: Option<String>,
248 user: U,
249}
250
251#[derive(Debug, Serialize)]
252pub(crate) struct BackupCodesResponse {
253 status: bool,
254 #[serde(rename = "backupCodes")]
255 backup_codes: Vec<String>,
256}
257
258#[derive(Debug, Serialize)]
259pub(crate) struct TwoFactorRedirectResponse {
260 #[serde(rename = "twoFactorRedirect")]
261 two_factor_redirect: bool,
262 #[serde(rename = "twoFactorMethods")]
265 two_factor_methods: Vec<&'static str>,
266}
267
268struct PendingTwoFactorState {
269 user: UserView,
270 key: String,
271 dont_remember: bool,
272}
273
274enum ResolvedTwoFactorState {
275 Session {
276 user: UserView,
277 session: Box<better_auth_core::wire::SessionView>,
278 key: String,
279 },
280 Pending(PendingTwoFactorState),
281}
282
283pub(crate) struct TrustedDeviceCheck {
284 pub trusted: bool,
285 pub set_cookie_headers: Vec<String>,
286}
287
288pub(crate) async fn inspect_trusted_device(
289 req: &AuthRequest,
290 user: &impl AuthUser,
291 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
292) -> AuthResult<TrustedDeviceCheck> {
293 let cookie_name = related_cookie_name(&ctx.config, TRUST_DEVICE_COOKIE_SUFFIX);
294 let Some(raw_cookie) = get_cookie(req, &cookie_name) else {
295 return Ok(TrustedDeviceCheck {
296 trusted: false,
297 set_cookie_headers: Vec::new(),
298 });
299 };
300
301 let Some(signed_value) = verify_signed_cookie_value(ctx.config.signing_secret(), &raw_cookie)?
302 .filter(|value| !value.is_empty())
303 else {
304 return Ok(TrustedDeviceCheck {
305 trusted: false,
306 set_cookie_headers: Vec::new(),
307 });
308 };
309
310 let Some((token, trust_identifier)) = signed_value.split_once('!') else {
311 return Ok(TrustedDeviceCheck {
312 trusted: false,
313 set_cookie_headers: vec![clear_cookie_header(
314 req,
315 &ctx.config,
316 TRUST_DEVICE_COOKIE_SUFFIX,
317 )?],
318 });
319 };
320
321 let expected_token = sign_value(
322 ctx.config.signing_secret(),
323 &format!("{}!{}", user.id().display_string()?, trust_identifier),
324 )?;
325 if token != expected_token {
326 return Ok(TrustedDeviceCheck {
327 trusted: false,
328 set_cookie_headers: vec![clear_cookie_header(
329 req,
330 &ctx.config,
331 TRUST_DEVICE_COOKIE_SUFFIX,
332 )?],
333 });
334 }
335
336 let Some(verification) = ctx
337 .database
338 .get_verification_by_identifier(trust_identifier)
339 .await?
340 else {
341 return Ok(TrustedDeviceCheck {
342 trusted: false,
343 set_cookie_headers: vec![clear_cookie_header(
344 req,
345 &ctx.config,
346 TRUST_DEVICE_COOKIE_SUFFIX,
347 )?],
348 });
349 };
350
351 if verification.value != user.id().into_owned()
352 || verification.expires_at.is_before_or_equal(Utc::now())?
353 {
354 return Ok(TrustedDeviceCheck {
355 trusted: false,
356 set_cookie_headers: vec![clear_cookie_header(
357 req,
358 &ctx.config,
359 TRUST_DEVICE_COOKIE_SUFFIX,
360 )?],
361 });
362 }
363
364 ctx.database
365 .delete_verification_by_identifier(trust_identifier)
366 .await?;
367
368 let rotated_cookie = create_trust_device_cookie_header(user, ctx).await?;
369 Ok(TrustedDeviceCheck {
370 trusted: true,
371 set_cookie_headers: vec![rotated_cookie],
372 })
373}
374
375pub(crate) async fn begin_sign_in_challenge(
376 user: &impl AuthUser,
377 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
378 headers: &mut better_auth_core::Headers,
379) -> AuthResult<TwoFactorRedirectResponse> {
380 let identifier = format!("2fa-{}", uuid::Uuid::new_v4());
381 let max_age = two_factor_cookie_max_age(ctx);
382 let expires_at = cookie_expires_at(max_age)?;
383 _ = ctx
384 .database
385 .create_verification(CreateVerification {
386 identifier: (identifier.clone()).into(),
387 value: user.id().into_owned(),
388 expires_at: expires_at.into(),
389 ..Default::default()
390 })
391 .await?;
392
393 let _ = ctx
394 .database
395 .create_verification(CreateVerification {
396 identifier: (format!("2fa-attempts-{identifier}")).into(),
397 value: ("0".to_owned()).into(),
398 expires_at: expires_at.into(),
399 ..Default::default()
400 })
401 .await?;
402
403 headers.append(
404 "Set-Cookie",
405 create_signed_cookie_header(
406 ctx.config.signing_secret(),
407 &ctx.config,
408 TWO_FACTOR_COOKIE_SUFFIX,
409 &identifier,
410 Some(max_age),
411 )?,
412 );
413
414 let mut two_factor_methods = Vec::new();
417 if !ctx
418 .get_metadata(METADATA_TOTP_DISABLED)
419 .and_then(serde_json::Value::as_bool)
420 .unwrap_or(false)
421 && ctx
422 .database
423 .get_two_factor_by_user_id(user.id().typed()?)
424 .await?
425 .is_some_and(|factor| factor.verified != Some(false))
426 {
427 two_factor_methods.push("totp");
428 }
429 if ctx
430 .get_metadata(METADATA_OTP_ENABLED)
431 .and_then(|value| value.as_bool())
432 .unwrap_or(false)
433 {
434 two_factor_methods.push("otp");
435 }
436
437 Ok(TwoFactorRedirectResponse {
438 two_factor_redirect: true,
439 two_factor_methods,
440 })
441}
442
443impl TwoFactorPlugin {
444 pub fn generate_totp(&self, secret: &str) -> AuthResult<String> {
446 require_totp(&self.config)?;
447 let inner = TOTP::new_unchecked(
448 Algorithm::SHA1,
449 if self.config.totp_digits == 0 {
450 6
451 } else {
452 self.config.totp_digits
453 },
454 1,
455 1,
456 secret.as_bytes().to_vec(),
457 None,
458 String::new(),
459 );
460 totp::Totp::new(inner, self.config.totp_period)
461 .with_default_period()
462 .generate_current()
463 }
464
465 pub fn custom_send_otp(mut self, sender: Arc<dyn SendTwoFactorOtp>) -> Self {
467 self.config.send_otp = Some(sender);
468 self
469 }
470
471 pub async fn view_backup_codes<S: better_auth_core::AuthSchema>(
477 &self,
478 user_id: &str,
479 ctx: &AuthContext<S>,
480 ) -> AuthResult<Vec<String>> {
481 view_backup_codes_core(user_id, &self.config.backup_code_options, ctx).await
482 }
483}
484
485#[async_trait]
486impl<S: better_auth_core::AuthSchema> better_auth_core::AuthPlugin<S> for TwoFactorPlugin {
487 fn name(&self) -> &'static str {
488 "two-factor"
489 }
490 async fn after_request(
491 &self,
492 req: &AuthRequest,
493 response: &mut AuthResponse,
494 ctx: &AuthContext<S>,
495 ) -> AuthResult<()> {
496 self.after_sign_in(req, response, ctx).await
497 }
498 fn routes(&self) -> Vec<better_auth_core::AuthRoute> {
499 let passwordless = self.config.allow_passwordless;
500 let totp_passwordless = self.config.totp_allow_passwordless.unwrap_or(passwordless);
501 let backup_passwordless = self
502 .config
503 .backup_code_options
504 .allow_passwordless
505 .unwrap_or(passwordless);
506 [
507 ("/two-factor/enable", "enableTwoFactor", passwordless),
508 ("/two-factor/disable", "disableTwoFactor", passwordless),
509 ("/two-factor/get-totp-uri", "getTOTPURI", totp_passwordless),
510 ("/two-factor/verify-totp", "verifyTOTP", passwordless),
511 ("/two-factor/send-otp", "sendTwoFactorOTP", passwordless),
512 ("/two-factor/verify-otp", "verifyTwoFactorOTP", passwordless),
513 (
514 "/two-factor/generate-backup-codes",
515 "generateBackupCodes",
516 backup_passwordless,
517 ),
518 (
519 "/two-factor/verify-backup-code",
520 "verifyBackupCode",
521 passwordless,
522 ),
523 ]
524 .into_iter()
525 .map(|(path, operation, optional)| {
526 better_auth_core::AuthRoute::post(path, operation)
527 .body_validator(move |req| request::validate(req, optional))
528 })
529 .collect()
530 }
531 async fn on_request(
532 &self,
533 req: &AuthRequest,
534 ctx: &AuthContext<S>,
535 ) -> AuthResult<Option<AuthResponse>> {
536 if *req.method() != better_auth_core::HttpMethod::Post {
537 return Ok(None);
538 }
539 let response = match req.path() {
540 "/two-factor/enable" => self.handle_enable(req, ctx).await?,
541 "/two-factor/disable" => self.handle_disable(req, ctx).await?,
542 "/two-factor/get-totp-uri" => self.handle_get_totp_uri(req, ctx).await?,
543 "/two-factor/verify-totp" => self.handle_verify_totp(req, ctx).await?,
544 "/two-factor/send-otp" => self.handle_send_otp(req, ctx).await?,
545 "/two-factor/verify-otp" => self.handle_verify_otp(req, ctx).await?,
546 "/two-factor/generate-backup-codes" => {
547 self.handle_generate_backup_codes(req, ctx).await?
548 }
549 "/two-factor/verify-backup-code" => self.handle_verify_backup_code(req, ctx).await?,
550 _ => return Ok(None),
551 };
552 Ok(Some(response))
553 }
554 async fn on_init(
555 &self,
556 ctx: &mut better_auth_core::AuthInitContext<S>,
557 ) -> better_auth_core::AuthResult<()> {
558 ctx.extensions.insert(self.config.clone());
559 S::User::require_plugin_fields("two-factor", &["two_factor_enabled"])?;
560 ctx.set_metadata(METADATA_ENABLED, serde_json::Value::Bool(true));
561 ctx.set_metadata(
562 METADATA_TOTP_DISABLED,
563 serde_json::Value::Bool(self.config.totp_disabled),
564 );
565 ctx.set_metadata(
566 METADATA_OTP_ENABLED,
567 serde_json::Value::Bool(
568 self.config.send_otp.is_some()
569 || ctx
570 .extensions
571 .get::<Arc<TwoFactorCallbacks<S>>>()
572 .is_some_and(|callbacks| callbacks.sender.is_some()),
573 ),
574 );
575 Ok(())
576 }
577}
578
579impl TwoFactorPlugin {
580 async fn handle_enable(
581 &self,
582 req: &AuthRequest,
583 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
584 ) -> AuthResult<AuthResponse> {
585 let body: EnableRequest = request::read(req, self.config.allow_passwordless)?;
586 let (user, session) = ctx.require_session(req).await?;
587
588 let (response, set_cookie_headers) =
589 enable_core(req, &body, &user, &session, &self.config, ctx).await?;
590 let mut auth_response = AuthResponse::json(200, &response)?;
591 for cookie in set_cookie_headers {
592 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
593 }
594 Ok(auth_response)
595 }
596
597 async fn handle_disable(
598 &self,
599 req: &AuthRequest,
600 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
601 ) -> AuthResult<AuthResponse> {
602 let body: DisableRequest = request::read(req, self.config.allow_passwordless)?;
603 let (user, session) = ctx.require_session(req).await?;
604
605 let (response, set_cookie_headers) =
606 disable_core(&body, &user, &session, req, &self.config, ctx).await?;
607 let mut auth_response = AuthResponse::json(200, &response)?;
608 for cookie in set_cookie_headers {
609 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
610 }
611 Ok(auth_response)
612 }
613
614 async fn handle_get_totp_uri(
615 &self,
616 req: &AuthRequest,
617 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
618 ) -> AuthResult<AuthResponse> {
619 let body: GetTotpUriRequest = request::read(
620 req,
621 self.config
622 .totp_allow_passwordless
623 .unwrap_or(self.config.allow_passwordless),
624 )?;
625 let (user, _session) = ctx.require_session(req).await?;
626
627 let response = get_totp_uri_core(&body, &user, &self.config, ctx).await?;
628 AuthResponse::json(200, &response).map_err(AuthError::from)
629 }
630
631 async fn handle_verify_totp(
632 &self,
633 req: &AuthRequest,
634 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
635 ) -> AuthResult<AuthResponse> {
636 let body: VerifyTotpRequest = request::read(req, false)?;
637
638 let (response, set_cookie_headers) =
639 verify_totp_core(req, &body, &self.config, ctx).await?;
640 let mut auth_response = AuthResponse::json(200, &response)?;
641 for cookie in set_cookie_headers {
642 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
643 }
644 Ok(auth_response)
645 }
646
647 async fn handle_send_otp(
648 &self,
649 req: &AuthRequest,
650 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
651 ) -> AuthResult<AuthResponse> {
652 let response = send_otp_core(req, &self.config, ctx).await?;
653 AuthResponse::json(200, &response).map_err(AuthError::from)
654 }
655
656 async fn handle_verify_otp(
657 &self,
658 req: &AuthRequest,
659 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
660 ) -> AuthResult<AuthResponse> {
661 let body: VerifyOtpRequest = request::read(req, false)?;
662
663 let (response, set_cookie_headers) = verify_otp_core(req, &body, &self.config, ctx).await?;
664 let mut auth_response = AuthResponse::json(200, &response)?;
665 for cookie in set_cookie_headers {
666 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
667 }
668 Ok(auth_response)
669 }
670
671 async fn handle_generate_backup_codes(
672 &self,
673 req: &AuthRequest,
674 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
675 ) -> AuthResult<AuthResponse> {
676 let body: GenerateBackupCodesRequest = request::read(
677 req,
678 self.config
679 .backup_code_options
680 .allow_passwordless
681 .unwrap_or(self.config.allow_passwordless),
682 )?;
683 let (user, _session) = ctx.require_session(req).await?;
684
685 let response = generate_backup_codes_core(&body, &user, &self.config, ctx).await?;
686 AuthResponse::json(200, &response).map_err(AuthError::from)
687 }
688
689 async fn handle_verify_backup_code(
690 &self,
691 req: &AuthRequest,
692 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
693 ) -> AuthResult<AuthResponse> {
694 let body: VerifyBackupCodeRequest = request::read(req, false)?;
695
696 let (response, set_cookie_headers) =
697 verify_backup_code_core(req, &body, &self.config, ctx).await?;
698 let mut auth_response = AuthResponse::json(200, &response)?;
699 for cookie in set_cookie_headers {
700 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
701 }
702 Ok(auth_response)
703 }
704}
705
706async fn resolve_two_factor_state<S: better_auth_core::AuthSchema>(
707 req: &AuthRequest,
708 ctx: &AuthContext<S>,
709) -> AuthResult<ResolvedTwoFactorState> {
710 match ctx.require_session(req).await {
711 Ok((user, session)) => {
712 let key = format!(
713 "{}!{}",
714 user.id().display_string()?,
715 session.id().display_string()?
716 );
717 return Ok(ResolvedTwoFactorState::Session {
718 user,
719 session: Box::new(session),
720 key,
721 });
722 }
723 Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => {}
724 Err(error) => return Err(error),
725 }
726
727 let identifier = read_signed_cookie(req, TWO_FACTOR_COOKIE_SUFFIX, ctx)?
728 .ok_or_else(|| AuthError::authentication_failed("Invalid two factor cookie"))?;
729 let verification = ctx
730 .database
731 .get_verification_by_identifier(&identifier)
732 .await?
733 .ok_or_else(|| AuthError::authentication_failed("Invalid two factor cookie"))?;
734 if verification.expires_at.is_before_or_equal(Utc::now())? {
735 ctx.database
736 .delete_verification_by_identifier(&identifier)
737 .await?;
738 return Err(AuthError::authentication_failed(
739 "Invalid two factor cookie",
740 ));
741 }
742
743 let user = ctx
744 .database
745 .get_user_by_id(verification.value.typed()?)
746 .await?
747 .ok_or_else(|| AuthError::authentication_failed("Invalid two factor cookie"))?;
748 let dont_remember = read_signed_cookie(req, DONT_REMEMBER_COOKIE_SUFFIX, ctx)?.is_some();
749
750 Ok(ResolvedTwoFactorState::Pending(PendingTwoFactorState {
751 user: ctx.internal_user_view(&user).await?,
752 key: identifier,
753 dont_remember,
754 }))
755}
756
757async fn verify_existing_session_factor(
758 req: &AuthRequest,
759 user: impl AuthUser,
760 session: impl AuthSession,
761 enrollment: Option<EnrollmentMethod>,
762 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
763 complete_enrollment: impl std::future::Future<Output = AuthResult<()>>,
764) -> AuthResult<(SessionTokenResponse<UserView>, Vec<String>)> {
765 if enrollment.is_some() && !user.two_factor_enabled() {
766 let updated_user = ctx
767 .database
768 .update_user(
769 user.id().typed()?,
770 UpdateUser {
771 two_factor_enabled: Some(true),
772 ..Default::default()
773 },
774 )
775 .await?;
776 let issued = issue_user_session(
777 ctx,
778 updated_user.id().typed()?,
779 session.ip_address().map(str::to_owned),
780 session.user_agent().map(str::to_owned),
781 )
782 .await
783 .map_err(SessionIssueError::into_auth_error)?;
784 let manager = ctx.session_manager();
785 manager
786 .set_session_cookie(
787 req,
788 manager
789 .internal_data(&updated_user, &issued.session)
790 .await?,
791 None,
792 )
793 .await?;
794 ctx.database.delete_session(session.token()).await?;
795 complete_enrollment.await?;
796 return Ok((
797 SessionTokenResponse {
798 token: Some(if enrollment == Some(EnrollmentMethod::Otp) {
799 issued.session.token().to_string()
800 } else {
801 session.token().to_string()
802 }),
803 user: if enrollment == Some(EnrollmentMethod::Otp) {
806 ctx.user_view(&updated_user).await?
807 } else {
808 ctx.user_view(&user).await?
809 },
810 },
811 Vec::new(),
812 ));
813 }
814
815 complete_enrollment.await?;
816 Ok((
817 SessionTokenResponse {
818 token: Some(session.token().to_string()),
819 user: ctx.user_view(&user).await?,
820 },
821 Vec::new(),
822 ))
823}
824
825async fn finalize_pending_two_factor<S: better_auth_core::AuthSchema>(
826 pending: PendingTwoFactorState,
827 req: &AuthRequest,
828 trust_device: bool,
829 ctx: &AuthContext<S>,
830) -> AuthResult<(SessionTokenResponse<UserView>, Vec<String>)> {
831 let Some(consumed) = ctx
832 .database
833 .consume_verification_by_identifier(&pending.key)
834 .await?
835 .filter(|verification| verification.value == pending.user.id)
836 else {
837 req.append_response_header(
838 "Set-Cookie",
839 clear_cookie_header(req, &ctx.config, TWO_FACTOR_COOKIE_SUFFIX)?,
840 )?;
841 return Err(AuthError::authentication_failed(
842 "Invalid two factor cookie",
843 ));
844 };
845 let meta = RequestMeta::from_request_with_config(req, &ctx.config.advanced.ip_address);
846 let expires_in = if pending.dont_remember {
847 Duration::days(1)
848 } else {
849 ctx.config.session.expires_in()
850 };
851 let issued = issue_user_session_with_lifetime(
852 ctx,
853 consumed.value.typed()?,
854 meta.ip_address,
855 meta.user_agent,
856 expires_in,
857 )
858 .await
859 .map_err(SessionIssueError::into_auth_error)?;
860
861 let manager = ctx.session_manager();
862 manager
863 .set_session_cookie(
864 req,
865 manager
866 .internal_data(&pending.user, &issued.session)
867 .await?,
868 None,
869 )
870 .await?;
871 let mut set_cookie_headers = vec![clear_cookie_header(
872 req,
873 &ctx.config,
874 TWO_FACTOR_COOKIE_SUFFIX,
875 )?];
876 if trust_device {
877 set_cookie_headers.push(create_trust_device_cookie_header(&issued.user, ctx).await?);
878 set_cookie_headers.push(clear_cookie_header(
879 req,
880 &ctx.config,
881 DONT_REMEMBER_COOKIE_SUFFIX,
882 )?);
883 }
884
885 Ok((
886 SessionTokenResponse {
887 token: Some(issued.session.token().to_string()),
888 user: ctx.user_view(&issued.user).await?,
889 },
890 set_cookie_headers,
891 ))
892}
893
894async fn load_two_factor_record(
895 user: &impl AuthUser,
896 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
897) -> AuthResult<TwoFactor> {
898 ctx.database
899 .get_two_factor_by_user_id(user.id().typed()?)
900 .await?
901 .ok_or_else(|| AuthError::bad_request("TOTP not enabled"))
902}
903
904impl ResolvedTwoFactorState {
905 fn is_sign_in(&self) -> bool {
906 matches!(self, Self::Pending(_))
907 }
908
909 fn user(&self) -> &UserView {
910 match self {
911 Self::Session { user, .. } => user,
912 Self::Pending(pending) => &pending.user,
913 }
914 }
915
916 fn key(&self) -> &str {
917 match self {
918 Self::Session { key, .. } => key,
919 Self::Pending(pending) => &pending.key,
920 }
921 }
922}