1use aes_gcm::aead::{Aead, KeyInit, OsRng};
2use aes_gcm::{AeadCore, Aes256Gcm, Key, Nonce};
3use async_trait::async_trait;
4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5use chrono::{Duration, Utc};
6use hkdf::Hkdf;
7use hmac::{Hmac, Mac};
8use rand::Rng;
9use rand::distributions::Alphanumeric;
10use serde::{Deserialize, Serialize};
11use sha2::Sha256;
12use std::sync::Arc;
13use totp_rs::{Algorithm, TOTP};
14use validator::Validate;
15
16use better_auth_core::entity::{AuthSession, AuthTwoFactor, AuthUser, AuthVerification};
17use better_auth_core::utils::cookie_utils::{
18 create_clear_cookie, create_session_cookie, create_session_cookie_with_max_age,
19 create_session_like_cookie, related_cookie_name,
20};
21use better_auth_core::wire::UserView;
22use better_auth_core::{
23 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, CreateTwoFactor,
24 CreateVerification, RequestMeta, TwoFactor, UpdateUser,
25};
26
27use crate::plugins::helpers::{
28 SessionIssueError, delete_session_cookie_headers, get_cookie, get_credential_password_hash,
29 issue_user_session,
30};
31
32use super::StatusResponse;
33
34#[cfg(test)]
35mod tests;
36
37const TWO_FACTOR_COOKIE_SUFFIX: &str = "two_factor";
38const TRUST_DEVICE_COOKIE_SUFFIX: &str = "trust_device";
39const DONT_REMEMBER_COOKIE_SUFFIX: &str = "dont_remember";
40
41const METADATA_ENABLED: &str = "two_factor.enabled";
42const METADATA_TWO_FACTOR_COOKIE_MAX_AGE: &str = "two_factor.two_factor_cookie_max_age";
43const METADATA_TRUST_DEVICE_MAX_AGE: &str = "two_factor.trust_device_max_age";
44
45const DEFAULT_TWO_FACTOR_COOKIE_MAX_AGE_SECS: i64 = 10 * 60;
46const DEFAULT_TRUST_DEVICE_MAX_AGE_SECS: i64 = 30 * 24 * 60 * 60;
47const DEFAULT_TOTP_PERIOD_SECS: u64 = 30;
48const DEFAULT_TOTP_DIGITS: usize = 6;
49const DEFAULT_OTP_DIGITS: usize = 6;
50const DEFAULT_OTP_LIFETIME_SECS: i64 = 3 * 60;
51const DEFAULT_OTP_ATTEMPT_LIMIT: usize = 5;
52const DEFAULT_BACKUP_CODE_COUNT: usize = 10;
53const DEFAULT_BACKUP_CODE_LENGTH: usize = 10;
54
55const ENCRYPTION_INFO: &[u8] = b"better-auth-two-factor-encryption";
56
57type HmacSha256 = Hmac<Sha256>;
58
59#[async_trait]
61pub trait SendTwoFactorOtp: Send + Sync {
62 async fn send(&self, user: &UserView, otp: &str) -> AuthResult<()>;
64}
65
66#[derive(Clone)]
68pub struct TwoFactorPlugin {
69 config: TwoFactorConfig,
70}
71
72#[derive(Clone, better_auth_core::PluginConfig)]
74#[plugin(name = "TwoFactorPlugin")]
75pub struct TwoFactorConfig {
76 #[config(default = None)]
78 pub issuer: Option<String>,
79 #[config(default = false)]
81 pub skip_verification_on_enable: bool,
82 #[config(default = DEFAULT_TWO_FACTOR_COOKIE_MAX_AGE_SECS)]
84 pub two_factor_cookie_max_age: i64,
85 #[config(default = DEFAULT_TRUST_DEVICE_MAX_AGE_SECS)]
87 pub trust_device_max_age: i64,
88 #[config(default = DEFAULT_TOTP_PERIOD_SECS)]
90 pub totp_period: u64,
91 #[config(default = DEFAULT_TOTP_DIGITS)]
93 pub totp_digits: usize,
94 #[config(default = None, skip)]
96 pub send_otp: Option<Arc<dyn SendTwoFactorOtp>>,
97}
98
99impl std::fmt::Debug for TwoFactorConfig {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("TwoFactorConfig")
102 .field("issuer", &self.issuer)
103 .field(
104 "skip_verification_on_enable",
105 &self.skip_verification_on_enable,
106 )
107 .field("two_factor_cookie_max_age", &self.two_factor_cookie_max_age)
108 .field("trust_device_max_age", &self.trust_device_max_age)
109 .field("totp_period", &self.totp_period)
110 .field("totp_digits", &self.totp_digits)
111 .field("send_otp", &self.send_otp.as_ref().map(|_| "custom"))
112 .finish()
113 }
114}
115
116#[derive(Debug, Deserialize, Validate)]
117pub(crate) struct EnableRequest {
118 password: String,
119 issuer: Option<String>,
120}
121
122#[derive(Debug, Deserialize, Validate)]
123pub(crate) struct DisableRequest {
124 password: String,
125}
126
127#[derive(Debug, Deserialize, Validate)]
128pub(crate) struct GetTotpUriRequest {
129 password: String,
130}
131
132#[derive(Debug, Deserialize, Validate)]
133pub(crate) struct VerifyTotpRequest {
134 code: String,
135 #[serde(rename = "trustDevice")]
136 trust_device: Option<bool>,
137}
138
139#[derive(Debug, Deserialize, Validate)]
140pub(crate) struct VerifyOtpRequest {
141 code: String,
142 #[serde(rename = "trustDevice")]
143 trust_device: Option<bool>,
144}
145
146#[derive(Debug, Deserialize, Validate)]
147pub(crate) struct GenerateBackupCodesRequest {
148 password: String,
149}
150
151#[derive(Debug, Deserialize, Validate)]
152pub(crate) struct VerifyBackupCodeRequest {
153 code: String,
154 #[serde(rename = "disableSession")]
155 disable_session: Option<bool>,
156 #[serde(rename = "trustDevice")]
157 trust_device: Option<bool>,
158}
159
160#[derive(Debug, Serialize)]
161pub(crate) struct EnableResponse {
162 #[serde(rename = "totpURI")]
163 totp_uri: String,
164 #[serde(rename = "backupCodes")]
165 backup_codes: Vec<String>,
166}
167
168#[derive(Debug, Serialize)]
169pub(crate) struct TotpUriResponse {
170 #[serde(rename = "totpURI")]
171 totp_uri: String,
172}
173
174#[derive(Debug, Serialize)]
175pub(crate) struct SessionTokenResponse<U: Serialize> {
176 token: String,
177 user: U,
178}
179
180#[derive(Debug, Serialize)]
181pub(crate) struct BackupCodesResponse {
182 status: bool,
183 #[serde(rename = "backupCodes")]
184 backup_codes: Vec<String>,
185}
186
187#[derive(Debug, Serialize)]
188pub(crate) struct TwoFactorRedirectResponse {
189 #[serde(rename = "twoFactorRedirect")]
190 two_factor_redirect: bool,
191}
192
193struct PendingTwoFactorState<S: better_auth_core::AuthSchema> {
194 user: S::User,
195 verification: S::Verification,
196 key: String,
197 dont_remember: bool,
198}
199
200enum ResolvedTwoFactorState<S: better_auth_core::AuthSchema> {
201 Session {
202 user: S::User,
203 session: S::Session,
204 key: String,
205 },
206 Pending(PendingTwoFactorState<S>),
207}
208
209pub(crate) struct SignInTwoFactorRedirect {
210 pub response: TwoFactorRedirectResponse,
211 pub set_cookie_headers: Vec<String>,
212}
213
214pub(crate) struct TrustedDeviceCheck {
215 pub trusted: bool,
216 pub set_cookie_headers: Vec<String>,
217}
218
219pub(crate) fn is_enabled(ctx: &AuthContext<impl better_auth_core::AuthSchema>) -> bool {
220 ctx.get_metadata(METADATA_ENABLED)
221 .and_then(|value| value.as_bool())
222 .unwrap_or(false)
223}
224
225pub(crate) async fn inspect_trusted_device(
226 req: &AuthRequest,
227 user: &impl AuthUser,
228 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
229) -> AuthResult<TrustedDeviceCheck> {
230 let cookie_name = related_cookie_name(&ctx.config, TRUST_DEVICE_COOKIE_SUFFIX);
231 let Some(raw_cookie) = get_cookie(req, &cookie_name) else {
232 return Ok(TrustedDeviceCheck {
233 trusted: false,
234 set_cookie_headers: Vec::new(),
235 });
236 };
237
238 let clear_header = create_clear_cookie(&cookie_name, &ctx.config);
239 let Some(signed_value) = verify_signed_cookie_value(&ctx.config.secret, &raw_cookie)? else {
240 return Ok(TrustedDeviceCheck {
241 trusted: false,
242 set_cookie_headers: vec![clear_header],
243 });
244 };
245
246 let Some((token, trust_identifier)) = signed_value.split_once('!') else {
247 return Ok(TrustedDeviceCheck {
248 trusted: false,
249 set_cookie_headers: vec![clear_header],
250 });
251 };
252
253 let expected_token = sign_value(
254 &ctx.config.secret,
255 &format!("{}!{}", user.id(), trust_identifier),
256 )?;
257 if token != expected_token {
258 return Ok(TrustedDeviceCheck {
259 trusted: false,
260 set_cookie_headers: vec![clear_header],
261 });
262 }
263
264 let Some(verification) = ctx
265 .database
266 .get_verification_by_identifier(trust_identifier)
267 .await?
268 else {
269 return Ok(TrustedDeviceCheck {
270 trusted: false,
271 set_cookie_headers: vec![clear_header],
272 });
273 };
274
275 if verification.value() != user.id().as_ref() || verification.expires_at() <= Utc::now() {
276 return Ok(TrustedDeviceCheck {
277 trusted: false,
278 set_cookie_headers: vec![clear_header],
279 });
280 }
281
282 ctx.database
283 .delete_verification(verification.id().as_ref())
284 .await?;
285
286 let rotated_cookie = create_trust_device_cookie_header(user, ctx).await?;
287 Ok(TrustedDeviceCheck {
288 trusted: true,
289 set_cookie_headers: vec![rotated_cookie],
290 })
291}
292
293pub(crate) async fn begin_sign_in_challenge(
294 user: &impl AuthUser,
295 remember_me: Option<bool>,
296 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
297) -> AuthResult<SignInTwoFactorRedirect> {
298 let identifier = format!("2fa-{}", uuid::Uuid::new_v4());
299 _ = ctx
300 .database
301 .create_verification(CreateVerification {
302 identifier: identifier.clone(),
303 value: user.id().to_string(),
304 expires_at: Utc::now() + Duration::seconds(two_factor_cookie_max_age(ctx)),
305 })
306 .await?;
307
308 let mut headers = delete_session_cookie_headers(&ctx.config);
309 headers.retain(|cookie| {
310 !cookie.starts_with(&format!(
311 "{}=",
312 related_cookie_name(&ctx.config, DONT_REMEMBER_COOKIE_SUFFIX)
313 ))
314 });
315 headers.push(create_signed_cookie_header(
316 &ctx.config.secret,
317 &ctx.config,
318 TWO_FACTOR_COOKIE_SUFFIX,
319 &identifier,
320 Some(two_factor_cookie_max_age(ctx)),
321 )?);
322
323 if remember_me == Some(false) {
324 headers.push(create_signed_cookie_header(
325 &ctx.config.secret,
326 &ctx.config,
327 DONT_REMEMBER_COOKIE_SUFFIX,
328 "true",
329 None,
330 )?);
331 }
332
333 Ok(SignInTwoFactorRedirect {
334 response: TwoFactorRedirectResponse {
335 two_factor_redirect: true,
336 },
337 set_cookie_headers: headers,
338 })
339}
340
341impl TwoFactorPlugin {
342 pub fn custom_send_otp(mut self, sender: Arc<dyn SendTwoFactorOtp>) -> Self {
344 self.config.send_otp = Some(sender);
345 self
346 }
347
348 pub async fn view_backup_codes<S: better_auth_core::AuthSchema>(
354 &self,
355 user_id: &str,
356 ctx: &AuthContext<S>,
357 ) -> AuthResult<Vec<String>> {
358 view_backup_codes_core(user_id, ctx).await
359 }
360}
361
362better_auth_core::impl_auth_plugin! {
363 TwoFactorPlugin, "two-factor";
364 routes {
365 post "/two-factor/enable" => handle_enable, "enable_two_factor";
366 post "/two-factor/disable" => handle_disable, "disable_two_factor";
367 post "/two-factor/get-totp-uri" => handle_get_totp_uri, "get_totp_uri";
368 post "/two-factor/verify-totp" => handle_verify_totp, "verify_totp";
369 post "/two-factor/send-otp" => handle_send_otp, "send_otp";
370 post "/two-factor/verify-otp" => handle_verify_otp, "verify_otp";
371 post "/two-factor/generate-backup-codes" => handle_generate_backup_codes, "generate_backup_codes";
372 post "/two-factor/verify-backup-code" => handle_verify_backup_code, "verify_backup_code";
373 }
374 extra {
375 async fn on_init(
376 &self,
377 ctx: &mut better_auth_core::AuthInitContext<S>,
378 ) -> better_auth_core::AuthResult<()> {
379 ctx.set_metadata(METADATA_ENABLED, serde_json::Value::Bool(true));
380 ctx.set_metadata(
381 METADATA_TWO_FACTOR_COOKIE_MAX_AGE,
382 serde_json::Value::Number(self.config.two_factor_cookie_max_age.into()),
383 );
384 ctx.set_metadata(
385 METADATA_TRUST_DEVICE_MAX_AGE,
386 serde_json::Value::Number(self.config.trust_device_max_age.into()),
387 );
388 Ok(())
389 }
390 }
391}
392
393impl TwoFactorPlugin {
394 async fn handle_enable(
395 &self,
396 req: &AuthRequest,
397 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
398 ) -> AuthResult<AuthResponse> {
399 let (user, session) = ctx.require_session(req).await?;
400 let body: EnableRequest = match better_auth_core::validate_request_body(req) {
401 Ok(v) => v,
402 Err(resp) => return Ok(resp),
403 };
404
405 let (response, set_cookie_headers) =
406 enable_core(&body, &user, &session, &self.config, ctx).await?;
407 let mut auth_response = AuthResponse::json(200, &response)?;
408 for cookie in set_cookie_headers {
409 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
410 }
411 Ok(auth_response)
412 }
413
414 async fn handle_disable(
415 &self,
416 req: &AuthRequest,
417 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
418 ) -> AuthResult<AuthResponse> {
419 let (user, session) = ctx.require_session(req).await?;
420 let body: DisableRequest = match better_auth_core::validate_request_body(req) {
421 Ok(v) => v,
422 Err(resp) => return Ok(resp),
423 };
424
425 let (response, set_cookie_headers) = disable_core(&body, &user, &session, req, ctx).await?;
426 let mut auth_response = AuthResponse::json(200, &response)?;
427 for cookie in set_cookie_headers {
428 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
429 }
430 Ok(auth_response)
431 }
432
433 async fn handle_get_totp_uri(
434 &self,
435 req: &AuthRequest,
436 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
437 ) -> AuthResult<AuthResponse> {
438 let (user, _session) = ctx.require_session(req).await?;
439 let body: GetTotpUriRequest = match better_auth_core::validate_request_body(req) {
440 Ok(v) => v,
441 Err(resp) => return Ok(resp),
442 };
443
444 let response = get_totp_uri_core(&body, &user, &self.config, ctx).await?;
445 AuthResponse::json(200, &response).map_err(AuthError::from)
446 }
447
448 async fn handle_verify_totp(
449 &self,
450 req: &AuthRequest,
451 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
452 ) -> AuthResult<AuthResponse> {
453 let body: VerifyTotpRequest = match better_auth_core::validate_request_body(req) {
454 Ok(v) => v,
455 Err(resp) => return Ok(resp),
456 };
457
458 let (response, set_cookie_headers) =
459 verify_totp_core(req, &body, &self.config, ctx).await?;
460 let mut auth_response = AuthResponse::json(200, &response)?;
461 for cookie in set_cookie_headers {
462 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
463 }
464 Ok(auth_response)
465 }
466
467 async fn handle_send_otp(
468 &self,
469 req: &AuthRequest,
470 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
471 ) -> AuthResult<AuthResponse> {
472 let response = send_otp_core(req, &self.config, ctx).await?;
473 AuthResponse::json(200, &response).map_err(AuthError::from)
474 }
475
476 async fn handle_verify_otp(
477 &self,
478 req: &AuthRequest,
479 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
480 ) -> AuthResult<AuthResponse> {
481 let body: VerifyOtpRequest = match better_auth_core::validate_request_body(req) {
482 Ok(v) => v,
483 Err(resp) => return Ok(resp),
484 };
485
486 let (response, set_cookie_headers) = verify_otp_core(req, &body, ctx).await?;
487 let mut auth_response = AuthResponse::json(200, &response)?;
488 for cookie in set_cookie_headers {
489 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
490 }
491 Ok(auth_response)
492 }
493
494 async fn handle_generate_backup_codes(
495 &self,
496 req: &AuthRequest,
497 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
498 ) -> AuthResult<AuthResponse> {
499 let (user, _session) = ctx.require_session(req).await?;
500 let body: GenerateBackupCodesRequest = match better_auth_core::validate_request_body(req) {
501 Ok(v) => v,
502 Err(resp) => return Ok(resp),
503 };
504
505 let response = generate_backup_codes_core(&body, &user, ctx).await?;
506 AuthResponse::json(200, &response).map_err(AuthError::from)
507 }
508
509 async fn handle_verify_backup_code(
510 &self,
511 req: &AuthRequest,
512 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
513 ) -> AuthResult<AuthResponse> {
514 let body: VerifyBackupCodeRequest = match better_auth_core::validate_request_body(req) {
515 Ok(v) => v,
516 Err(resp) => return Ok(resp),
517 };
518
519 let (response, set_cookie_headers) = verify_backup_code_core(req, &body, ctx).await?;
520 let mut auth_response = AuthResponse::json(200, &response)?;
521 for cookie in set_cookie_headers {
522 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
523 }
524 Ok(auth_response)
525 }
526}
527
528async fn enable_core(
529 body: &EnableRequest,
530 user: &impl AuthUser,
531 current_session: &impl AuthSession,
532 config: &TwoFactorConfig,
533 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
534) -> AuthResult<(EnableResponse, Vec<String>)> {
535 verify_user_password(ctx, user, &body.password).await?;
536
537 let _ = ctx.database.delete_two_factor(user.id().as_ref()).await;
538
539 let secret = generate_secret();
540 let encrypted_secret = encrypt_value(&ctx.config.secret, &secret)?;
541 let backup_codes = generate_backup_codes();
542 let encrypted_backup_codes =
543 encrypt_value(&ctx.config.secret, &serde_json::to_string(&backup_codes)?)?;
544
545 _ = ctx
546 .database
547 .create_two_factor(CreateTwoFactor {
548 user_id: user.id().to_string(),
549 secret: encrypted_secret,
550 backup_codes: encrypted_backup_codes,
551 })
552 .await?;
553
554 let mut set_cookie_headers = Vec::new();
555 if config.skip_verification_on_enable {
556 let updated_user = ctx
557 .database
558 .update_user(
559 user.id().as_ref(),
560 UpdateUser {
561 two_factor_enabled: Some(true),
562 ..Default::default()
563 },
564 )
565 .await?;
566 let issued = issue_user_session(
567 ctx,
568 updated_user.id().as_ref(),
569 current_session.ip_address().map(str::to_owned),
570 current_session.user_agent().map(str::to_owned),
571 )
572 .await
573 .map_err(SessionIssueError::into_auth_error)?;
574 ctx.database.delete_session(current_session.token()).await?;
575 set_cookie_headers.push(create_session_cookie(issued.session.token(), &ctx.config));
576 }
577
578 let totp_uri = build_totp(config, &secret, body.issuer.as_deref(), user, ctx)?.get_url();
579 Ok((
580 EnableResponse {
581 totp_uri,
582 backup_codes,
583 },
584 set_cookie_headers,
585 ))
586}
587
588async fn disable_core(
589 body: &DisableRequest,
590 user: &impl AuthUser,
591 current_session: &impl AuthSession,
592 req: &AuthRequest,
593 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
594) -> AuthResult<(StatusResponse, Vec<String>)> {
595 verify_user_password(ctx, user, &body.password).await?;
596
597 ctx.database.delete_two_factor(user.id().as_ref()).await?;
598
599 let updated_user = ctx
600 .database
601 .update_user(
602 user.id().as_ref(),
603 UpdateUser {
604 two_factor_enabled: Some(false),
605 ..Default::default()
606 },
607 )
608 .await?;
609
610 let issued = issue_user_session(
611 ctx,
612 updated_user.id().as_ref(),
613 current_session.ip_address().map(str::to_owned),
614 current_session.user_agent().map(str::to_owned),
615 )
616 .await
617 .map_err(SessionIssueError::into_auth_error)?;
618 ctx.database.delete_session(current_session.token()).await?;
619
620 let mut set_cookie_headers = vec![create_session_cookie(issued.session.token(), &ctx.config)];
621
622 if let Some(trust_cookie) = read_signed_cookie(req, TRUST_DEVICE_COOKIE_SUFFIX, ctx)? {
623 if let Some((_, trust_identifier)) = trust_cookie.split_once('!')
624 && let Some(verification) = ctx
625 .database
626 .get_verification_by_identifier(trust_identifier)
627 .await?
628 {
629 let _ = ctx
630 .database
631 .delete_verification(verification.id().as_ref())
632 .await;
633 }
634 set_cookie_headers.push(clear_cookie_header(&ctx.config, TRUST_DEVICE_COOKIE_SUFFIX));
635 }
636
637 Ok((StatusResponse { status: true }, set_cookie_headers))
638}
639
640async fn get_totp_uri_core(
641 body: &GetTotpUriRequest,
642 user: &impl AuthUser,
643 config: &TwoFactorConfig,
644 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
645) -> AuthResult<TotpUriResponse> {
646 verify_user_password(ctx, user, &body.password).await?;
647 let two_factor = load_two_factor_record(user, ctx).await?;
648 let secret = decrypt_value(&ctx.config.secret, two_factor.secret())?;
649 Ok(TotpUriResponse {
650 totp_uri: build_totp(config, &secret, None, user, ctx)?.get_url(),
651 })
652}
653
654async fn verify_totp_core(
655 req: &AuthRequest,
656 body: &VerifyTotpRequest,
657 config: &TwoFactorConfig,
658 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
659) -> AuthResult<(SessionTokenResponse<UserView>, Vec<String>)> {
660 let state = resolve_two_factor_state(req, ctx).await?;
661 let two_factor = load_two_factor_record(state.user(), ctx).await?;
662 let secret = decrypt_value(&ctx.config.secret, two_factor.secret())?;
663 let totp = build_totp(config, &secret, None, state.user(), ctx)?;
664
665 if !totp
666 .check_current(&body.code)
667 .map_err(|error| AuthError::internal(format!("Failed to verify TOTP: {}", error)))?
668 {
669 return Err(AuthError::authentication_failed("Invalid code"));
670 }
671
672 match state {
673 ResolvedTwoFactorState::Session { user, session, .. } => {
674 verify_existing_session_factor(user, session, true, ctx).await
675 }
676 ResolvedTwoFactorState::Pending(pending) => {
677 finalize_pending_two_factor(pending, req, body.trust_device.unwrap_or(false), true, ctx)
678 .await
679 }
680 }
681}
682
683async fn send_otp_core(
684 req: &AuthRequest,
685 config: &TwoFactorConfig,
686 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
687) -> AuthResult<StatusResponse> {
688 let sender = config
689 .send_otp
690 .as_ref()
691 .ok_or_else(|| AuthError::bad_request("otp isn't configured"))?;
692 let state = resolve_two_factor_state(req, ctx).await?;
693
694 let otp = format!(
695 "{:0width$}",
696 rand::thread_rng().gen_range(0..10u32.pow(DEFAULT_OTP_DIGITS as u32)),
697 width = DEFAULT_OTP_DIGITS
698 );
699 let hashed_otp = better_auth_core::hash_password(None, &otp).await?;
700 let identifier = otp_verification_identifier(state.key());
701
702 if let Some(existing) = ctx
703 .database
704 .get_verification_by_identifier(&identifier)
705 .await?
706 {
707 ctx.database
708 .delete_verification(existing.id().as_ref())
709 .await?;
710 }
711
712 _ = ctx
713 .database
714 .create_verification(CreateVerification {
715 identifier,
716 value: format!("{}:0", hashed_otp),
717 expires_at: Utc::now() + Duration::seconds(DEFAULT_OTP_LIFETIME_SECS),
718 })
719 .await?;
720
721 if let Err(error) = sender.send(&UserView::from(state.user()), &otp).await {
722 tracing::warn!(error = %error, "Failed to send two-factor OTP");
723 }
724
725 Ok(StatusResponse { status: true })
726}
727
728async fn verify_otp_core(
729 req: &AuthRequest,
730 body: &VerifyOtpRequest,
731 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
732) -> AuthResult<(SessionTokenResponse<UserView>, Vec<String>)> {
733 let state = resolve_two_factor_state(req, ctx).await?;
734 let identifier = otp_verification_identifier(state.key());
735 let Some(verification) = ctx
736 .database
737 .get_verification_by_identifier(&identifier)
738 .await?
739 else {
740 return Err(AuthError::bad_request("OTP has expired"));
741 };
742
743 if verification.expires_at() <= Utc::now() {
744 ctx.database
745 .delete_verification(verification.id().as_ref())
746 .await?;
747 return Err(AuthError::bad_request("OTP has expired"));
748 }
749
750 let Some((stored_hash, counter)) = verification.value().rsplit_once(':') else {
751 return Err(AuthError::internal("Malformed OTP verification payload"));
752 };
753
754 let attempts = counter.parse::<usize>().map_err(|error| {
755 AuthError::internal(format!("Malformed OTP attempt counter: {}", error))
756 })?;
757 if attempts >= DEFAULT_OTP_ATTEMPT_LIMIT {
758 ctx.database
759 .delete_verification(verification.id().as_ref())
760 .await?;
761 return Err(AuthError::bad_request(
762 "Too many attempts. Please request a new code.",
763 ));
764 }
765
766 let is_valid = match better_auth_core::verify_password(None, &body.code, stored_hash).await {
767 Ok(()) => true,
768 Err(AuthError::InvalidCredentials) => false,
769 Err(error) => return Err(error),
770 };
771
772 if !is_valid {
773 let next_value = format!("{}:{}", stored_hash, attempts + 1);
774 let expires_at = verification.expires_at();
775 let verification_identifier = verification.identifier().to_string();
776 ctx.database
777 .delete_verification(verification.id().as_ref())
778 .await?;
779 _ = ctx
780 .database
781 .create_verification(CreateVerification {
782 identifier: verification_identifier,
783 value: next_value,
784 expires_at,
785 })
786 .await?;
787 return Err(AuthError::authentication_failed("Invalid code"));
788 }
789
790 ctx.database
791 .delete_verification(verification.id().as_ref())
792 .await?;
793
794 match state {
795 ResolvedTwoFactorState::Session { user, session, .. } => {
796 verify_existing_session_factor(user, session, true, ctx).await
797 }
798 ResolvedTwoFactorState::Pending(pending) => {
799 finalize_pending_two_factor(pending, req, body.trust_device.unwrap_or(false), true, ctx)
800 .await
801 }
802 }
803}
804
805async fn generate_backup_codes_core(
806 body: &GenerateBackupCodesRequest,
807 user: &impl AuthUser,
808 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
809) -> AuthResult<BackupCodesResponse> {
810 if !user.two_factor_enabled() {
811 return Err(AuthError::bad_request("Two factor isn't enabled"));
812 }
813
814 verify_user_password(ctx, user, &body.password).await?;
815 let _ = load_two_factor_record(user, ctx).await?;
816
817 let backup_codes = generate_backup_codes();
818 let encrypted = encrypt_value(&ctx.config.secret, &serde_json::to_string(&backup_codes)?)?;
819 _ = ctx
820 .database
821 .update_two_factor_backup_codes(user.id().as_ref(), &encrypted)
822 .await?;
823
824 Ok(BackupCodesResponse {
825 status: true,
826 backup_codes,
827 })
828}
829
830async fn verify_backup_code_core(
831 req: &AuthRequest,
832 body: &VerifyBackupCodeRequest,
833 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
834) -> AuthResult<(SessionTokenResponse<UserView>, Vec<String>)> {
835 let state = resolve_two_factor_state(req, ctx).await?;
836 let two_factor = ctx
837 .database
838 .get_two_factor_by_user_id(state.user().id().as_ref())
839 .await?
840 .ok_or_else(|| AuthError::bad_request("Backup codes aren't enabled"))?;
841
842 let Some(mut backup_codes) =
843 decrypt_backup_codes(two_factor.backup_codes(), &ctx.config.secret)?
844 else {
845 return Err(AuthError::authentication_failed("Invalid backup code"));
846 };
847 let Some(index) = backup_codes
848 .iter()
849 .position(|candidate| candidate == &body.code)
850 else {
851 return Err(AuthError::authentication_failed("Invalid backup code"));
852 };
853 let _ = backup_codes.remove(index);
854
855 let encrypted = encrypt_value(&ctx.config.secret, &serde_json::to_string(&backup_codes)?)?;
856 _ = ctx
857 .database
858 .update_two_factor_backup_codes(state.user().id().as_ref(), &encrypted)
859 .await?;
860
861 match state {
862 ResolvedTwoFactorState::Session { user, session, .. } => {
863 if body.disable_session.unwrap_or(false) {
864 Ok((
865 SessionTokenResponse {
866 token: session.token().to_string(),
867 user: UserView::from(&user),
868 },
869 Vec::new(),
870 ))
871 } else {
872 verify_existing_session_factor(user, session, false, ctx).await
873 }
874 }
875 ResolvedTwoFactorState::Pending(pending) => {
876 finalize_pending_two_factor(
877 pending,
878 req,
879 body.trust_device.unwrap_or(false),
880 !body.disable_session.unwrap_or(false),
881 ctx,
882 )
883 .await
884 }
885 }
886}
887
888async fn view_backup_codes_core<S: better_auth_core::AuthSchema>(
889 user_id: &str,
890 ctx: &AuthContext<S>,
891) -> AuthResult<Vec<String>> {
892 let two_factor = ctx
893 .database
894 .get_two_factor_by_user_id(user_id)
895 .await?
896 .ok_or_else(|| AuthError::bad_request("Backup codes aren't enabled"))?;
897 let Some(backup_codes) = decrypt_backup_codes(two_factor.backup_codes(), &ctx.config.secret)?
898 else {
899 return Err(AuthError::bad_request("Invalid backup code"));
900 };
901 Ok(backup_codes)
902}
903
904async fn resolve_two_factor_state<S: better_auth_core::AuthSchema>(
905 req: &AuthRequest,
906 ctx: &AuthContext<S>,
907) -> AuthResult<ResolvedTwoFactorState<S>> {
908 if let Ok((user, session)) = ctx.require_session(req).await {
909 let key = format!("{}!{}", user.id(), session.id());
910 return Ok(ResolvedTwoFactorState::Session { user, session, key });
911 }
912
913 let identifier = read_signed_cookie(req, TWO_FACTOR_COOKIE_SUFFIX, ctx)?
914 .ok_or_else(|| AuthError::authentication_failed("Invalid two factor cookie"))?;
915 let verification = ctx
916 .database
917 .get_verification_by_identifier(&identifier)
918 .await?
919 .ok_or_else(|| AuthError::authentication_failed("Invalid two factor cookie"))?;
920 if verification.expires_at() <= Utc::now() {
921 ctx.database
922 .delete_verification(verification.id().as_ref())
923 .await?;
924 return Err(AuthError::authentication_failed(
925 "Invalid two factor cookie",
926 ));
927 }
928
929 let user = ctx
930 .database
931 .get_user_by_id(verification.value())
932 .await?
933 .ok_or_else(|| AuthError::authentication_failed("Invalid two factor cookie"))?;
934 let dont_remember = read_signed_cookie(req, DONT_REMEMBER_COOKIE_SUFFIX, ctx)?.is_some();
935
936 Ok(ResolvedTwoFactorState::Pending(PendingTwoFactorState {
937 user,
938 verification,
939 key: identifier,
940 dont_remember,
941 }))
942}
943
944async fn verify_existing_session_factor(
945 user: impl AuthUser,
946 session: impl AuthSession,
947 enable_two_factor_if_needed: bool,
948 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
949) -> AuthResult<(SessionTokenResponse<UserView>, Vec<String>)> {
950 if enable_two_factor_if_needed && !user.two_factor_enabled() {
951 let updated_user = ctx
952 .database
953 .update_user(
954 user.id().as_ref(),
955 UpdateUser {
956 two_factor_enabled: Some(true),
957 ..Default::default()
958 },
959 )
960 .await?;
961 let issued = issue_user_session(
962 ctx,
963 updated_user.id().as_ref(),
964 session.ip_address().map(str::to_owned),
965 session.user_agent().map(str::to_owned),
966 )
967 .await
968 .map_err(SessionIssueError::into_auth_error)?;
969 ctx.database.delete_session(session.token()).await?;
970 return Ok((
971 SessionTokenResponse {
972 token: issued.session.token().to_string(),
973 user: UserView::from(&user),
976 },
977 vec![create_session_cookie(issued.session.token(), &ctx.config)],
978 ));
979 }
980
981 Ok((
982 SessionTokenResponse {
983 token: session.token().to_string(),
984 user: UserView::from(&user),
985 },
986 Vec::new(),
987 ))
988}
989
990async fn finalize_pending_two_factor<S: better_auth_core::AuthSchema>(
991 pending: PendingTwoFactorState<S>,
992 req: &AuthRequest,
993 trust_device: bool,
994 set_session_cookie: bool,
995 ctx: &AuthContext<S>,
996) -> AuthResult<(SessionTokenResponse<UserView>, Vec<String>)> {
997 let meta = RequestMeta::from_request(req);
998 let issued = issue_user_session(
999 ctx,
1000 pending.user.id().as_ref(),
1001 meta.ip_address,
1002 meta.user_agent,
1003 )
1004 .await
1005 .map_err(SessionIssueError::into_auth_error)?;
1006 ctx.database
1007 .delete_verification(pending.verification.id().as_ref())
1008 .await?;
1009
1010 let mut set_cookie_headers = vec![clear_cookie_header(&ctx.config, TWO_FACTOR_COOKIE_SUFFIX)];
1011 if set_session_cookie {
1012 set_cookie_headers.push(create_session_cookie_for_dont_remember(
1013 issued.session.token(),
1014 pending.dont_remember,
1015 &ctx.config,
1016 ));
1017 if pending.dont_remember {
1018 set_cookie_headers.push(create_signed_cookie_header(
1019 &ctx.config.secret,
1020 &ctx.config,
1021 DONT_REMEMBER_COOKIE_SUFFIX,
1022 "true",
1023 None,
1024 )?);
1025 }
1026 }
1027 if trust_device {
1028 set_cookie_headers.push(create_trust_device_cookie_header(&issued.user, ctx).await?);
1029 set_cookie_headers.push(clear_cookie_header(
1030 &ctx.config,
1031 DONT_REMEMBER_COOKIE_SUFFIX,
1032 ));
1033 }
1034
1035 Ok((
1036 SessionTokenResponse {
1037 token: issued.session.token().to_string(),
1038 user: UserView::from(&issued.user),
1039 },
1040 set_cookie_headers,
1041 ))
1042}
1043
1044async fn load_two_factor_record(
1045 user: &impl AuthUser,
1046 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
1047) -> AuthResult<TwoFactor> {
1048 ctx.database
1049 .get_two_factor_by_user_id(user.id().as_ref())
1050 .await?
1051 .ok_or_else(|| AuthError::bad_request("TOTP not enabled"))
1052}
1053
1054fn build_totp(
1055 config: &TwoFactorConfig,
1056 secret: &str,
1057 request_issuer: Option<&str>,
1058 user: &impl AuthUser,
1059 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
1060) -> AuthResult<TOTP> {
1061 let issuer = request_issuer
1062 .map(str::to_owned)
1063 .or_else(|| config.issuer.clone())
1064 .unwrap_or_else(|| ctx.config.app_name.clone());
1065 let account_name = user.email().unwrap_or("user").to_string();
1066 TOTP::new(
1067 Algorithm::SHA1,
1068 config.totp_digits,
1069 1,
1070 config.totp_period,
1071 secret.as_bytes().to_vec(),
1072 Some(issuer),
1073 account_name,
1074 )
1075 .map_err(|error| AuthError::internal(format!("Failed to create TOTP: {}", error)))
1076}
1077
1078async fn verify_user_password(
1079 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
1080 user: &impl AuthUser,
1081 password: &str,
1082) -> AuthResult<()> {
1083 let stored_hash = get_credential_password_hash(ctx, user)
1084 .await?
1085 .ok_or_else(|| AuthError::bad_request("Invalid password"))?;
1086 match better_auth_core::verify_password(None, password, &stored_hash).await {
1087 Ok(()) => Ok(()),
1088 Err(AuthError::InvalidCredentials) => Err(AuthError::bad_request("Invalid password")),
1089 Err(error) => Err(error),
1090 }
1091}
1092
1093fn generate_secret() -> String {
1094 rand::thread_rng()
1095 .sample_iter(&Alphanumeric)
1096 .take(32)
1097 .map(char::from)
1098 .collect()
1099}
1100
1101fn generate_backup_codes() -> Vec<String> {
1102 (0..DEFAULT_BACKUP_CODE_COUNT)
1103 .map(|_| {
1104 rand::thread_rng()
1105 .sample_iter(&Alphanumeric)
1106 .take(DEFAULT_BACKUP_CODE_LENGTH)
1107 .map(char::from)
1108 .collect::<String>()
1109 })
1110 .map(|code| format!("{}-{}", &code[..5], &code[5..]))
1111 .collect()
1112}
1113
1114fn decrypt_backup_codes(backup_codes: &str, secret: &str) -> AuthResult<Option<Vec<String>>> {
1115 let decrypted = decrypt_value(secret, backup_codes)?;
1116 serde_json::from_str(&decrypted)
1117 .ok()
1118 .map_or(Ok(None), |codes| Ok(Some(codes)))
1119}
1120
1121fn otp_verification_identifier(key: &str) -> String {
1122 format!("2fa-otp-{}", key)
1123}
1124
1125fn two_factor_cookie_max_age(ctx: &AuthContext<impl better_auth_core::AuthSchema>) -> i64 {
1126 ctx.get_metadata(METADATA_TWO_FACTOR_COOKIE_MAX_AGE)
1127 .and_then(|value| value.as_i64())
1128 .unwrap_or(DEFAULT_TWO_FACTOR_COOKIE_MAX_AGE_SECS)
1129}
1130
1131fn trust_device_max_age(ctx: &AuthContext<impl better_auth_core::AuthSchema>) -> i64 {
1132 ctx.get_metadata(METADATA_TRUST_DEVICE_MAX_AGE)
1133 .and_then(|value| value.as_i64())
1134 .unwrap_or(DEFAULT_TRUST_DEVICE_MAX_AGE_SECS)
1135}
1136
1137fn create_session_cookie_for_dont_remember(
1138 token: &str,
1139 dont_remember: bool,
1140 config: &better_auth_core::AuthConfig,
1141) -> String {
1142 if dont_remember {
1143 create_session_cookie_with_max_age(Some(token), None, config)
1144 } else {
1145 create_session_cookie(token, config)
1146 }
1147}
1148
1149fn clear_cookie_header(config: &better_auth_core::AuthConfig, suffix: &str) -> String {
1150 create_clear_cookie(&related_cookie_name(config, suffix), config)
1151}
1152
1153async fn create_trust_device_cookie_header(
1154 user: &impl AuthUser,
1155 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
1156) -> AuthResult<String> {
1157 let identifier = format!("trust-device-{}", uuid::Uuid::new_v4());
1158 let token = sign_value(&ctx.config.secret, &format!("{}!{}", user.id(), identifier))?;
1159 let value = format!("{}!{}", token, identifier);
1160 let expires_at = Utc::now() + Duration::seconds(trust_device_max_age(ctx));
1161 _ = ctx
1162 .database
1163 .create_verification(CreateVerification {
1164 identifier: identifier.clone(),
1165 value: user.id().to_string(),
1166 expires_at,
1167 })
1168 .await?;
1169 create_signed_cookie_header(
1170 &ctx.config.secret,
1171 &ctx.config,
1172 TRUST_DEVICE_COOKIE_SUFFIX,
1173 &value,
1174 Some(trust_device_max_age(ctx)),
1175 )
1176}
1177
1178fn create_signed_cookie_header(
1179 secret: &str,
1180 config: &better_auth_core::AuthConfig,
1181 suffix: &str,
1182 value: &str,
1183 max_age_seconds: Option<i64>,
1184) -> AuthResult<String> {
1185 let cookie_name = related_cookie_name(config, suffix);
1186 let signed_value = sign_cookie_value(secret, value)?;
1187 Ok(create_session_like_cookie(
1188 &cookie_name,
1189 &signed_value,
1190 max_age_seconds,
1191 config,
1192 ))
1193}
1194
1195fn read_signed_cookie<S: better_auth_core::AuthSchema>(
1196 req: &AuthRequest,
1197 suffix: &str,
1198 ctx: &AuthContext<S>,
1199) -> AuthResult<Option<String>> {
1200 let cookie_name = related_cookie_name(&ctx.config, suffix);
1201 let Some(raw_cookie) = get_cookie(req, &cookie_name) else {
1202 return Ok(None);
1203 };
1204 verify_signed_cookie_value(&ctx.config.secret, &raw_cookie)
1205}
1206
1207fn sign_cookie_value(secret: &str, value: &str) -> AuthResult<String> {
1208 Ok(format!("{}.{}", value, sign_value(secret, value)?))
1209}
1210
1211fn verify_signed_cookie_value(secret: &str, signed_value: &str) -> AuthResult<Option<String>> {
1212 let Some((value, signature)) = signed_value.rsplit_once('.') else {
1213 return Ok(None);
1214 };
1215 Ok(verify_signature(secret, value, signature)?.then(|| value.to_string()))
1216}
1217
1218fn sign_value(secret: &str, value: &str) -> AuthResult<String> {
1219 let mut mac = <HmacSha256 as Mac>::new_from_slice(secret.as_bytes())
1220 .map_err(|error| AuthError::internal(format!("Failed to initialize HMAC: {}", error)))?;
1221 mac.update(value.as_bytes());
1222 Ok(URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()))
1223}
1224
1225fn verify_signature(secret: &str, value: &str, signature: &str) -> AuthResult<bool> {
1226 let decoded = match URL_SAFE_NO_PAD.decode(signature) {
1227 Ok(decoded) => decoded,
1228 Err(_) => return Ok(false),
1229 };
1230 let mut mac = <HmacSha256 as Mac>::new_from_slice(secret.as_bytes())
1231 .map_err(|error| AuthError::internal(format!("Failed to initialize HMAC: {}", error)))?;
1232 mac.update(value.as_bytes());
1233 Ok(mac.verify_slice(&decoded).is_ok())
1234}
1235
1236fn derive_encryption_key(secret: &str) -> AuthResult<Key<Aes256Gcm>> {
1237 let hkdf = Hkdf::<Sha256>::new(None, secret.as_bytes());
1238 let mut okm = [0u8; 32];
1239 hkdf.expand(ENCRYPTION_INFO, &mut okm).map_err(|error| {
1240 AuthError::internal(format!("Failed to derive encryption key: {}", error))
1241 })?;
1242 Ok(*Key::<Aes256Gcm>::from_slice(&okm))
1243}
1244
1245fn encrypt_value(secret: &str, plaintext: &str) -> AuthResult<String> {
1246 let cipher = Aes256Gcm::new(&derive_encryption_key(secret)?);
1247 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
1248 let ciphertext = cipher
1249 .encrypt(&nonce, plaintext.as_bytes())
1250 .map_err(|error| {
1251 AuthError::internal(format!("Failed to encrypt two-factor data: {}", error))
1252 })?;
1253 let mut output = nonce.to_vec();
1254 output.extend_from_slice(&ciphertext);
1255 Ok(URL_SAFE_NO_PAD.encode(output))
1256}
1257
1258fn decrypt_value(secret: &str, encrypted: &str) -> AuthResult<String> {
1259 let cipher = Aes256Gcm::new(&derive_encryption_key(secret)?);
1260 let bytes = URL_SAFE_NO_PAD.decode(encrypted).map_err(|error| {
1261 AuthError::internal(format!(
1262 "Failed to decode encrypted two-factor data: {}",
1263 error
1264 ))
1265 })?;
1266 if bytes.len() < 12 {
1267 return Err(AuthError::internal(
1268 "Encrypted two-factor payload is missing the nonce",
1269 ));
1270 }
1271 let (nonce_bytes, ciphertext) = bytes.split_at(12);
1272 let plaintext = cipher
1273 .decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
1274 .map_err(|error| {
1275 AuthError::internal(format!("Failed to decrypt two-factor data: {}", error))
1276 })?;
1277 String::from_utf8(plaintext).map_err(|error| {
1278 AuthError::internal(format!(
1279 "Two-factor plaintext is not valid UTF-8: {}",
1280 error
1281 ))
1282 })
1283}
1284
1285impl<S: better_auth_core::AuthSchema> ResolvedTwoFactorState<S> {
1286 fn user(&self) -> &S::User {
1287 match self {
1288 Self::Session { user, .. } => user,
1289 Self::Pending(pending) => &pending.user,
1290 }
1291 }
1292
1293 fn key(&self) -> &str {
1294 match self {
1295 Self::Session { key, .. } => key,
1296 Self::Pending(pending) => &pending.key,
1297 }
1298 }
1299}