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