Skip to main content

better_auth_api/plugins/
two_factor.rs

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
16/// Two-factor authentication plugin providing TOTP, OTP, and backup code flows.
17pub 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// -- Request types --
37
38#[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// -- Response types --
87
88#[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
122// -- Free-standing helpers --
123
124fn 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
170// -- Core functions (session-based) --
171
172async 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    // Generate TOTP secret
181    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    // Generate and hash backup codes
194    let backup_codes = generate_backup_codes(config);
195    let hashed_codes = hash_backup_codes(&backup_codes).await?;
196
197    // Store 2FA record
198    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    // Update user flag
207    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    // Generate new codes
281    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
294// -- Session / auth helpers --
295
296/// Extract the user_id from a `2fa_xxx` pending verification token.
297async 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
332// -- Core functions (pending-2fa) --
333
334/// Returns `(VerifyTotpResponse<DB::User>, session_token)`.
335async 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    // Code valid — create session
364    let session = ctx
365        .session_manager()
366        .create_session(user, None, None)
367        .await?;
368
369    // Delete the pending verification
370    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    // Generate 6-digit OTP
386    use rand::Rng;
387    let otp: String = format!("{:06}", rand::thread_rng().gen_range(0..1_000_000u32));
388
389    // Store the OTP verification (expires in 5 minutes)
390    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    // Send via email if provider is available
400    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
412/// Returns `(VerifyTotpResponse<DB::User>, session_token)`.
413async 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    // Look up the OTP verification
420    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    // Valid — create session
436    let session = ctx
437        .session_manager()
438        .create_session(user, None, None)
439        .await?;
440
441    // Clean up verifications
442    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
456/// Returns `(VerifyBackupCodeResponse<DB::User, DB::Session>, session_token)`.
457async 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    // Try to match the provided code against each hashed code
477    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    // Remove used code and update
492    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    // Create session
503    let session = ctx
504        .session_manager()
505        .create_session(user, None, None)
506        .await?;
507
508    // Clean up pending verification
509    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
519// -- Old-style handlers (delegating to core functions) --
520
521impl 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    // -- Session-based handlers --
669
670    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    // -- Pending-2fa handlers --
714
715    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}