1use serde::{Deserialize, Serialize};
2use totp_rs::{Algorithm, Secret, TOTP};
3use validator::Validate;
4
5use better_auth_core::adapters::DatabaseAdapter;
6use better_auth_core::entity::{AuthSession, AuthTwoFactor, AuthUser, AuthVerification};
7use better_auth_core::{AuthContext, AuthError, AuthResult};
8use better_auth_core::{
9 AuthRequest, AuthResponse, CreateTwoFactor, CreateVerification, UpdateUser,
10};
11
12use better_auth_core::utils::cookie_utils::create_session_cookie;
13
14use super::StatusResponse;
15
16pub struct TwoFactorPlugin {
18 config: TwoFactorConfig,
19}
20
21#[derive(Debug, Clone, better_auth_core::PluginConfig)]
22#[plugin(name = "TwoFactorPlugin")]
23pub struct TwoFactorConfig {
24 #[config(default = "BetterAuth".to_string())]
25 pub issuer: String,
26 #[config(default = 10)]
27 pub backup_code_count: usize,
28 #[config(default = 8)]
29 pub backup_code_length: usize,
30 #[config(default = 30)]
31 pub totp_period: u64,
32 #[config(default = 6)]
33 pub totp_digits: usize,
34}
35
36#[derive(Debug, Deserialize, Validate)]
39pub(crate) struct EnableRequest {
40 password: String,
41 issuer: Option<String>,
42}
43
44#[derive(Debug, Deserialize, Validate)]
45pub(crate) struct DisableRequest {
46 password: String,
47}
48
49#[derive(Debug, Deserialize, Validate)]
50pub(crate) struct GetTotpUriRequest {
51 password: String,
52}
53
54#[derive(Debug, Deserialize, Validate)]
55pub(crate) struct VerifyTotpRequest {
56 code: String,
57 #[serde(rename = "trustDevice")]
58 #[allow(dead_code)]
59 trust_device: Option<String>,
60}
61
62#[derive(Debug, Deserialize, Validate)]
63pub(crate) struct VerifyOtpRequest {
64 code: String,
65 #[serde(rename = "trustDevice")]
66 #[allow(dead_code)]
67 trust_device: Option<String>,
68}
69
70#[derive(Debug, Deserialize, Validate)]
71pub(crate) struct GenerateBackupCodesRequest {
72 password: String,
73}
74
75#[derive(Debug, Deserialize, Validate)]
76pub(crate) struct VerifyBackupCodeRequest {
77 code: String,
78 #[serde(rename = "disableSession")]
79 #[allow(dead_code)]
80 disable_session: Option<String>,
81 #[serde(rename = "trustDevice")]
82 #[allow(dead_code)]
83 trust_device: Option<String>,
84}
85
86#[derive(Debug, Serialize)]
89pub(crate) struct EnableResponse {
90 #[serde(rename = "totpURI")]
91 totp_uri: String,
92 #[serde(rename = "backupCodes")]
93 backup_codes: Vec<String>,
94}
95
96#[derive(Debug, Serialize)]
97pub(crate) struct TotpUriResponse {
98 #[serde(rename = "totpURI")]
99 totp_uri: String,
100}
101
102#[derive(Debug, Serialize)]
103pub(crate) struct VerifyTotpResponse<U: Serialize> {
104 status: bool,
105 token: String,
106 user: U,
107}
108
109#[derive(Debug, Serialize)]
110pub(crate) struct VerifyBackupCodeResponse<U: Serialize, S: Serialize> {
111 user: U,
112 session: S,
113}
114
115#[derive(Debug, Serialize)]
116pub(crate) struct BackupCodesResponse {
117 status: bool,
118 #[serde(rename = "backupCodes")]
119 backup_codes: Vec<String>,
120}
121
122fn generate_backup_codes(config: &TwoFactorConfig) -> Vec<String> {
125 use rand::Rng;
126 (0..config.backup_code_count)
127 .map(|_| {
128 rand::thread_rng()
129 .sample_iter(&rand::distributions::Alphanumeric)
130 .take(config.backup_code_length)
131 .map(char::from)
132 .collect::<String>()
133 .to_uppercase()
134 })
135 .collect()
136}
137
138async fn hash_backup_codes(codes: &[String]) -> AuthResult<String> {
139 let mut hashed = Vec::with_capacity(codes.len());
140 for code in codes {
141 hashed.push(better_auth_core::hash_password(None, code).await?);
142 }
143 serde_json::to_string(&hashed).map_err(|e| AuthError::internal(e.to_string()))
144}
145
146fn build_totp(
147 config: &TwoFactorConfig,
148 secret: &[u8],
149 email: &str,
150 issuer: &str,
151) -> AuthResult<TOTP> {
152 TOTP::new(
153 Algorithm::SHA1,
154 config.totp_digits,
155 1,
156 config.totp_period,
157 secret.to_vec(),
158 Some(issuer.to_string()),
159 email.to_string(),
160 )
161 .map_err(|e| AuthError::internal(format!("Failed to create TOTP: {}", e)))
162}
163
164async fn verify_user_password<U: AuthUser>(user: &U, password: &str) -> AuthResult<()> {
165 let stored_hash = user.password_hash().ok_or(AuthError::InvalidCredentials)?;
166
167 better_auth_core::verify_password(None, password, stored_hash).await
168}
169
170async fn enable_core<DB: DatabaseAdapter>(
173 body: &EnableRequest,
174 user: &DB::User,
175 config: &TwoFactorConfig,
176 ctx: &AuthContext<DB>,
177) -> AuthResult<EnableResponse> {
178 verify_user_password(user, &body.password).await?;
179
180 let secret = Secret::generate_secret();
182 let secret_encoded = secret.to_encoded().to_string();
183 let secret_bytes = secret
184 .to_bytes()
185 .map_err(|e| AuthError::internal(format!("Failed to convert secret to bytes: {}", e)))?;
186
187 let issuer = body.issuer.as_deref().unwrap_or(&config.issuer);
188 let email = user.email().unwrap_or("user");
189
190 let totp = build_totp(config, &secret_bytes, email, issuer)?;
191 let totp_uri = totp.get_url();
192
193 let backup_codes = generate_backup_codes(config);
195 let hashed_codes = hash_backup_codes(&backup_codes).await?;
196
197 ctx.database
199 .create_two_factor(CreateTwoFactor {
200 user_id: user.id().to_string(),
201 secret: secret_encoded,
202 backup_codes: Some(hashed_codes),
203 })
204 .await?;
205
206 ctx.database
208 .update_user(
209 user.id(),
210 UpdateUser {
211 two_factor_enabled: Some(true),
212 ..Default::default()
213 },
214 )
215 .await?;
216
217 Ok(EnableResponse {
218 totp_uri,
219 backup_codes,
220 })
221}
222
223async fn disable_core<DB: DatabaseAdapter>(
224 body: &DisableRequest,
225 user: &DB::User,
226 ctx: &AuthContext<DB>,
227) -> AuthResult<StatusResponse> {
228 verify_user_password(user, &body.password).await?;
229
230 ctx.database.delete_two_factor(user.id()).await?;
231
232 ctx.database
233 .update_user(
234 user.id(),
235 UpdateUser {
236 two_factor_enabled: Some(false),
237 ..Default::default()
238 },
239 )
240 .await?;
241
242 Ok(StatusResponse { status: true })
243}
244
245async fn get_totp_uri_core<DB: DatabaseAdapter>(
246 body: &GetTotpUriRequest,
247 user: &DB::User,
248 config: &TwoFactorConfig,
249 ctx: &AuthContext<DB>,
250) -> AuthResult<TotpUriResponse> {
251 verify_user_password(user, &body.password).await?;
252
253 let two_factor = ctx
254 .database
255 .get_two_factor_by_user_id(user.id())
256 .await?
257 .ok_or_else(|| AuthError::not_found("Two-factor authentication not enabled"))?;
258
259 let secret = Secret::Encoded(two_factor.secret().to_string());
260 let secret_bytes = secret
261 .to_bytes()
262 .map_err(|e| AuthError::internal(format!("Failed to decode secret: {}", e)))?;
263
264 let email = user.email().unwrap_or("user");
265 let totp = build_totp(config, &secret_bytes, email, &config.issuer)?;
266
267 Ok(TotpUriResponse {
268 totp_uri: totp.get_url(),
269 })
270}
271
272async fn generate_backup_codes_core<DB: DatabaseAdapter>(
273 body: &GenerateBackupCodesRequest,
274 user: &DB::User,
275 config: &TwoFactorConfig,
276 ctx: &AuthContext<DB>,
277) -> AuthResult<BackupCodesResponse> {
278 verify_user_password(user, &body.password).await?;
279
280 let backup_codes = generate_backup_codes(config);
282 let hashed_codes = hash_backup_codes(&backup_codes).await?;
283
284 ctx.database
285 .update_two_factor_backup_codes(user.id(), &hashed_codes)
286 .await?;
287
288 Ok(BackupCodesResponse {
289 status: true,
290 backup_codes,
291 })
292}
293
294async fn get_pending_2fa_user<DB: DatabaseAdapter>(
298 req: &AuthRequest,
299 ctx: &AuthContext<DB>,
300) -> AuthResult<(DB::User, String)> {
301 let token = req
302 .headers
303 .get("authorization")
304 .and_then(|v| v.strip_prefix("Bearer "))
305 .ok_or(AuthError::Unauthenticated)?;
306
307 if !token.starts_with("2fa_") {
308 return Err(AuthError::bad_request("Invalid 2FA pending token"));
309 }
310
311 let identifier = format!("2fa_pending:{}", token);
312 let verification = ctx
313 .database
314 .get_verification_by_identifier(&identifier)
315 .await?
316 .ok_or_else(|| AuthError::bad_request("Invalid or expired 2FA token"))?;
317
318 if verification.expires_at() < chrono::Utc::now() {
319 return Err(AuthError::bad_request("2FA token expired"));
320 }
321
322 let user_id = verification.value();
323 let user = ctx
324 .database
325 .get_user_by_id(user_id)
326 .await?
327 .ok_or(AuthError::UserNotFound)?;
328
329 Ok((user, verification.id().to_string()))
330}
331
332async fn verify_totp_core<DB: DatabaseAdapter>(
336 body: &VerifyTotpRequest,
337 user: &DB::User,
338 verification_id: &str,
339 config: &TwoFactorConfig,
340 ctx: &AuthContext<DB>,
341) -> AuthResult<(VerifyTotpResponse<DB::User>, String)> {
342 let two_factor = ctx
343 .database
344 .get_two_factor_by_user_id(user.id())
345 .await?
346 .ok_or_else(|| AuthError::not_found("Two-factor authentication not enabled"))?;
347
348 let secret = Secret::Encoded(two_factor.secret().to_string());
349 let secret_bytes = secret
350 .to_bytes()
351 .map_err(|e| AuthError::internal(format!("Failed to decode secret: {}", e)))?;
352
353 let email = user.email().unwrap_or("user");
354 let totp = build_totp(config, &secret_bytes, email, &config.issuer)?;
355
356 if !totp
357 .check_current(&body.code)
358 .map_err(|e| AuthError::internal(format!("TOTP check error: {}", e)))?
359 {
360 return Err(AuthError::bad_request("Invalid TOTP code"));
361 }
362
363 let session = ctx
365 .session_manager()
366 .create_session(user, None, None)
367 .await?;
368
369 ctx.database.delete_verification(verification_id).await?;
371
372 let token = session.token().to_string();
373 let response = VerifyTotpResponse {
374 status: true,
375 token: token.clone(),
376 user: user.clone(),
377 };
378 Ok((response, token))
379}
380
381async fn send_otp_core<DB: DatabaseAdapter>(
382 user: &DB::User,
383 ctx: &AuthContext<DB>,
384) -> AuthResult<StatusResponse> {
385 use rand::Rng;
387 let otp: String = format!("{:06}", rand::thread_rng().gen_range(0..1_000_000u32));
388
389 let expires_at = chrono::Utc::now() + chrono::Duration::minutes(5);
391 ctx.database
392 .create_verification(CreateVerification {
393 identifier: format!("2fa_otp:{}", user.id()),
394 value: otp.clone(),
395 expires_at,
396 })
397 .await?;
398
399 if let Some(email) = user.email()
401 && let Ok(provider) = ctx.email_provider()
402 {
403 let body = format!("Your 2FA verification code is: {}", otp);
404 let _ = provider
405 .send(email, "Your verification code", &body, &body)
406 .await;
407 }
408
409 Ok(StatusResponse { status: true })
410}
411
412async fn verify_otp_core<DB: DatabaseAdapter>(
414 body: &VerifyOtpRequest,
415 user: &DB::User,
416 verification_id: &str,
417 ctx: &AuthContext<DB>,
418) -> AuthResult<(VerifyTotpResponse<DB::User>, String)> {
419 let otp_identifier = format!("2fa_otp:{}", user.id());
421 let otp_verification = ctx
422 .database
423 .get_verification_by_identifier(&otp_identifier)
424 .await?
425 .ok_or_else(|| AuthError::bad_request("No OTP found. Please request a new one."))?;
426
427 if otp_verification.expires_at() < chrono::Utc::now() {
428 return Err(AuthError::bad_request("OTP has expired"));
429 }
430
431 if otp_verification.value() != body.code {
432 return Err(AuthError::bad_request("Invalid OTP code"));
433 }
434
435 let session = ctx
437 .session_manager()
438 .create_session(user, None, None)
439 .await?;
440
441 ctx.database
443 .delete_verification(otp_verification.id())
444 .await?;
445 ctx.database.delete_verification(verification_id).await?;
446
447 let token = session.token().to_string();
448 let response = VerifyTotpResponse {
449 status: true,
450 token: token.clone(),
451 user: user.clone(),
452 };
453 Ok((response, token))
454}
455
456async fn verify_backup_code_core<DB: DatabaseAdapter>(
458 body: &VerifyBackupCodeRequest,
459 user: &DB::User,
460 verification_id: &str,
461 ctx: &AuthContext<DB>,
462) -> AuthResult<(VerifyBackupCodeResponse<DB::User, DB::Session>, String)> {
463 let two_factor = ctx
464 .database
465 .get_two_factor_by_user_id(user.id())
466 .await?
467 .ok_or_else(|| AuthError::not_found("Two-factor authentication not enabled"))?;
468
469 let codes_json = two_factor
470 .backup_codes()
471 .ok_or_else(|| AuthError::bad_request("No backup codes available"))?;
472
473 let hashed_codes: Vec<String> = serde_json::from_str(codes_json)
474 .map_err(|e| AuthError::internal(format!("Failed to parse backup codes: {}", e)))?;
475
476 let mut matched_index: Option<usize> = None;
478
479 for (i, hash_str) in hashed_codes.iter().enumerate() {
480 if better_auth_core::verify_password(None, &body.code, hash_str)
481 .await
482 .is_ok()
483 {
484 matched_index = Some(i);
485 break;
486 }
487 }
488
489 let idx = matched_index.ok_or_else(|| AuthError::bad_request("Invalid backup code"))?;
490
491 let mut remaining_codes = hashed_codes;
493 remaining_codes.remove(idx);
494
495 let updated_codes_json =
496 serde_json::to_string(&remaining_codes).map_err(|e| AuthError::internal(e.to_string()))?;
497
498 ctx.database
499 .update_two_factor_backup_codes(user.id(), &updated_codes_json)
500 .await?;
501
502 let session = ctx
504 .session_manager()
505 .create_session(user, None, None)
506 .await?;
507
508 ctx.database.delete_verification(verification_id).await?;
510
511 let token = session.token().to_string();
512 let response = VerifyBackupCodeResponse {
513 user: user.clone(),
514 session,
515 };
516 Ok((response, token))
517}
518
519impl TwoFactorPlugin {
522 async fn handle_enable<DB: DatabaseAdapter>(
523 &self,
524 req: &AuthRequest,
525 ctx: &AuthContext<DB>,
526 ) -> AuthResult<AuthResponse> {
527 let (user, _session) = ctx.require_session(req).await?;
528 let body: EnableRequest = match better_auth_core::validate_request_body(req) {
529 Ok(v) => v,
530 Err(resp) => return Ok(resp),
531 };
532 let response = enable_core(&body, &user, &self.config, ctx).await?;
533 AuthResponse::json(200, &response).map_err(AuthError::from)
534 }
535
536 async fn handle_disable<DB: DatabaseAdapter>(
537 &self,
538 req: &AuthRequest,
539 ctx: &AuthContext<DB>,
540 ) -> AuthResult<AuthResponse> {
541 let (user, _session) = ctx.require_session(req).await?;
542 let body: DisableRequest = match better_auth_core::validate_request_body(req) {
543 Ok(v) => v,
544 Err(resp) => return Ok(resp),
545 };
546 let response = disable_core(&body, &user, ctx).await?;
547 AuthResponse::json(200, &response).map_err(AuthError::from)
548 }
549
550 async fn handle_get_totp_uri<DB: DatabaseAdapter>(
551 &self,
552 req: &AuthRequest,
553 ctx: &AuthContext<DB>,
554 ) -> AuthResult<AuthResponse> {
555 let (user, _session) = ctx.require_session(req).await?;
556 let body: GetTotpUriRequest = match better_auth_core::validate_request_body(req) {
557 Ok(v) => v,
558 Err(resp) => return Ok(resp),
559 };
560 let response = get_totp_uri_core(&body, &user, &self.config, ctx).await?;
561 AuthResponse::json(200, &response).map_err(AuthError::from)
562 }
563
564 async fn handle_verify_totp<DB: DatabaseAdapter>(
565 &self,
566 req: &AuthRequest,
567 ctx: &AuthContext<DB>,
568 ) -> AuthResult<AuthResponse> {
569 let (user, verification_id) = get_pending_2fa_user(req, ctx).await?;
570 let body: VerifyTotpRequest = match better_auth_core::validate_request_body(req) {
571 Ok(v) => v,
572 Err(resp) => return Ok(resp),
573 };
574 let (response, token) =
575 verify_totp_core(&body, &user, &verification_id, &self.config, ctx).await?;
576 let cookie_header = create_session_cookie(&token, &ctx.config);
577 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
578 }
579
580 async fn handle_send_otp<DB: DatabaseAdapter>(
581 &self,
582 req: &AuthRequest,
583 ctx: &AuthContext<DB>,
584 ) -> AuthResult<AuthResponse> {
585 let (user, _verification_id) = get_pending_2fa_user(req, ctx).await?;
586 let response = send_otp_core(&user, ctx).await?;
587 AuthResponse::json(200, &response).map_err(AuthError::from)
588 }
589
590 async fn handle_verify_otp<DB: DatabaseAdapter>(
591 &self,
592 req: &AuthRequest,
593 ctx: &AuthContext<DB>,
594 ) -> AuthResult<AuthResponse> {
595 let (user, verification_id) = get_pending_2fa_user(req, ctx).await?;
596 let body: VerifyOtpRequest = match better_auth_core::validate_request_body(req) {
597 Ok(v) => v,
598 Err(resp) => return Ok(resp),
599 };
600 let (response, token) = verify_otp_core(&body, &user, &verification_id, ctx).await?;
601 let cookie_header = create_session_cookie(&token, &ctx.config);
602 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
603 }
604
605 async fn handle_generate_backup_codes<DB: DatabaseAdapter>(
606 &self,
607 req: &AuthRequest,
608 ctx: &AuthContext<DB>,
609 ) -> AuthResult<AuthResponse> {
610 let (user, _session) = ctx.require_session(req).await?;
611 let body: GenerateBackupCodesRequest = match better_auth_core::validate_request_body(req) {
612 Ok(v) => v,
613 Err(resp) => return Ok(resp),
614 };
615 let response = generate_backup_codes_core(&body, &user, &self.config, ctx).await?;
616 AuthResponse::json(200, &response).map_err(AuthError::from)
617 }
618
619 async fn handle_verify_backup_code<DB: DatabaseAdapter>(
620 &self,
621 req: &AuthRequest,
622 ctx: &AuthContext<DB>,
623 ) -> AuthResult<AuthResponse> {
624 let (user, verification_id) = get_pending_2fa_user(req, ctx).await?;
625 let body: VerifyBackupCodeRequest = match better_auth_core::validate_request_body(req) {
626 Ok(v) => v,
627 Err(resp) => return Ok(resp),
628 };
629 let (response, token) =
630 verify_backup_code_core(&body, &user, &verification_id, ctx).await?;
631 let cookie_header = create_session_cookie(&token, &ctx.config);
632 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
633 }
634}
635
636better_auth_core::impl_auth_plugin! {
637 TwoFactorPlugin, "two-factor";
638 routes {
639 post "/two-factor/enable" => handle_enable, "enable_two_factor";
640 post "/two-factor/disable" => handle_disable, "disable_two_factor";
641 post "/two-factor/get-totp-uri" => handle_get_totp_uri, "get_totp_uri";
642 post "/two-factor/verify-totp" => handle_verify_totp, "verify_totp";
643 post "/two-factor/send-otp" => handle_send_otp, "send_otp";
644 post "/two-factor/verify-otp" => handle_verify_otp, "verify_otp";
645 post "/two-factor/generate-backup-codes" => handle_generate_backup_codes, "generate_backup_codes";
646 post "/two-factor/verify-backup-code" => handle_verify_backup_code, "verify_backup_code";
647 }
648}
649
650#[cfg(feature = "axum")]
651mod axum_impl {
652 use super::*;
653 use std::sync::Arc;
654
655 use axum::Json;
656 use axum::extract::{Extension, State};
657 use axum::http::header;
658 use axum::response::IntoResponse;
659 use better_auth_core::error::AuthError;
660 use better_auth_core::extractors::{CurrentSession, Pending2faToken, ValidatedJson};
661 use better_auth_core::plugin::AuthState;
662
663 #[derive(Clone)]
664 struct PluginState {
665 config: TwoFactorConfig,
666 }
667
668 async fn handle_enable<DB: DatabaseAdapter>(
671 State(state): State<AuthState<DB>>,
672 Extension(ps): Extension<Arc<PluginState>>,
673 CurrentSession { user, .. }: CurrentSession<DB>,
674 ValidatedJson(body): ValidatedJson<EnableRequest>,
675 ) -> Result<Json<EnableResponse>, AuthError> {
676 let ctx = state.to_context();
677 let result = enable_core(&body, &user, &ps.config, &ctx).await?;
678 Ok(Json(result))
679 }
680
681 async fn handle_disable<DB: DatabaseAdapter>(
682 State(state): State<AuthState<DB>>,
683 CurrentSession { user, .. }: CurrentSession<DB>,
684 ValidatedJson(body): ValidatedJson<DisableRequest>,
685 ) -> Result<Json<StatusResponse>, AuthError> {
686 let ctx = state.to_context();
687 let result = disable_core(&body, &user, &ctx).await?;
688 Ok(Json(result))
689 }
690
691 async fn handle_get_totp_uri<DB: DatabaseAdapter>(
692 State(state): State<AuthState<DB>>,
693 Extension(ps): Extension<Arc<PluginState>>,
694 CurrentSession { user, .. }: CurrentSession<DB>,
695 ValidatedJson(body): ValidatedJson<GetTotpUriRequest>,
696 ) -> Result<Json<TotpUriResponse>, AuthError> {
697 let ctx = state.to_context();
698 let result = get_totp_uri_core(&body, &user, &ps.config, &ctx).await?;
699 Ok(Json(result))
700 }
701
702 async fn handle_generate_backup_codes<DB: DatabaseAdapter>(
703 State(state): State<AuthState<DB>>,
704 Extension(ps): Extension<Arc<PluginState>>,
705 CurrentSession { user, .. }: CurrentSession<DB>,
706 ValidatedJson(body): ValidatedJson<GenerateBackupCodesRequest>,
707 ) -> Result<Json<BackupCodesResponse>, AuthError> {
708 let ctx = state.to_context();
709 let result = generate_backup_codes_core(&body, &user, &ps.config, &ctx).await?;
710 Ok(Json(result))
711 }
712
713 async fn handle_verify_totp<DB: DatabaseAdapter>(
716 State(state): State<AuthState<DB>>,
717 Extension(ps): Extension<Arc<PluginState>>,
718 Pending2faToken {
719 user,
720 verification_id,
721 }: Pending2faToken<DB>,
722 ValidatedJson(body): ValidatedJson<VerifyTotpRequest>,
723 ) -> Result<impl IntoResponse, AuthError> {
724 let ctx = state.to_context();
725 let (response, token) =
726 verify_totp_core(&body, &user, &verification_id, &ps.config, &ctx).await?;
727 let cookie = state.session_cookie(&token);
728 Ok(([(header::SET_COOKIE, cookie)], Json(response)))
729 }
730
731 async fn handle_send_otp<DB: DatabaseAdapter>(
732 State(state): State<AuthState<DB>>,
733 Pending2faToken { user, .. }: Pending2faToken<DB>,
734 ) -> Result<Json<StatusResponse>, AuthError> {
735 let ctx = state.to_context();
736 let result = send_otp_core(&user, &ctx).await?;
737 Ok(Json(result))
738 }
739
740 async fn handle_verify_otp<DB: DatabaseAdapter>(
741 State(state): State<AuthState<DB>>,
742 Pending2faToken {
743 user,
744 verification_id,
745 }: Pending2faToken<DB>,
746 ValidatedJson(body): ValidatedJson<VerifyOtpRequest>,
747 ) -> Result<impl IntoResponse, AuthError> {
748 let ctx = state.to_context();
749 let (response, token) = verify_otp_core(&body, &user, &verification_id, &ctx).await?;
750 let cookie = state.session_cookie(&token);
751 Ok(([(header::SET_COOKIE, cookie)], Json(response)))
752 }
753
754 async fn handle_verify_backup_code<DB: DatabaseAdapter>(
755 State(state): State<AuthState<DB>>,
756 Pending2faToken {
757 user,
758 verification_id,
759 }: Pending2faToken<DB>,
760 ValidatedJson(body): ValidatedJson<VerifyBackupCodeRequest>,
761 ) -> Result<impl IntoResponse, AuthError> {
762 let ctx = state.to_context();
763 let (response, token) =
764 verify_backup_code_core(&body, &user, &verification_id, &ctx).await?;
765 let cookie = state.session_cookie(&token);
766 Ok(([(header::SET_COOKIE, cookie)], Json(response)))
767 }
768
769 impl<DB: DatabaseAdapter> better_auth_core::AxumPlugin<DB> for TwoFactorPlugin {
770 fn name(&self) -> &'static str {
771 "two-factor"
772 }
773
774 fn router(&self) -> axum::Router<AuthState<DB>> {
775 use axum::routing::post;
776
777 let plugin_state = Arc::new(PluginState {
778 config: self.config.clone(),
779 });
780 axum::Router::new()
781 .route("/two-factor/enable", post(handle_enable::<DB>))
782 .route("/two-factor/disable", post(handle_disable::<DB>))
783 .route("/two-factor/get-totp-uri", post(handle_get_totp_uri::<DB>))
784 .route("/two-factor/verify-totp", post(handle_verify_totp::<DB>))
785 .route("/two-factor/send-otp", post(handle_send_otp::<DB>))
786 .route("/two-factor/verify-otp", post(handle_verify_otp::<DB>))
787 .route(
788 "/two-factor/generate-backup-codes",
789 post(handle_generate_backup_codes::<DB>),
790 )
791 .route(
792 "/two-factor/verify-backup-code",
793 post(handle_verify_backup_code::<DB>),
794 )
795 .layer(Extension(plugin_state))
796 }
797 }
798}