1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use std::sync::Arc;
4use validator::Validate;
5
6use better_auth_core::adapters::DatabaseAdapter;
7use better_auth_core::entity::{AuthSession, AuthUser};
8use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
9use better_auth_core::{AuthError, AuthResult};
10use better_auth_core::{
11 AuthRequest, AuthResponse, CreateUser, CreateVerification, HttpMethod, PASSWORD_HASH_KEY,
12};
13
14use super::email_verification::EmailVerificationPlugin;
15use better_auth_core::utils::cookie_utils::create_session_cookie;
16use better_auth_core::utils::password::{self as password_utils, PasswordHasher};
17pub struct EmailPasswordPlugin {
19 config: EmailPasswordConfig,
20 email_verification: Option<Arc<EmailVerificationPlugin>>,
23}
24
25#[derive(Clone)]
26pub struct EmailPasswordConfig {
27 pub enable_signup: bool,
28 pub require_email_verification: bool,
29 pub password_min_length: usize,
30 pub password_max_length: usize,
32 pub auto_sign_in: bool,
35 pub password_hasher: Option<Arc<dyn PasswordHasher>>,
37}
38
39impl std::fmt::Debug for EmailPasswordConfig {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("EmailPasswordConfig")
42 .field("enable_signup", &self.enable_signup)
43 .field(
44 "require_email_verification",
45 &self.require_email_verification,
46 )
47 .field("password_min_length", &self.password_min_length)
48 .field("password_max_length", &self.password_max_length)
49 .field("auto_sign_in", &self.auto_sign_in)
50 .field(
51 "password_hasher",
52 &self.password_hasher.as_ref().map(|_| "custom"),
53 )
54 .finish()
55 }
56}
57
58#[derive(Debug, Deserialize, Validate)]
59#[allow(dead_code)]
60pub(crate) struct SignUpRequest {
61 #[validate(length(min = 1, message = "Name is required"))]
62 name: String,
63 #[validate(email(message = "Invalid email address"))]
64 email: String,
65 #[validate(length(min = 1, message = "Password is required"))]
66 password: String,
67 username: Option<String>,
68 #[serde(rename = "displayUsername")]
69 display_username: Option<String>,
70 #[serde(rename = "callbackURL")]
71 callback_url: Option<String>,
72}
73
74#[derive(Debug, Deserialize, Validate)]
75#[allow(dead_code)]
76pub(crate) struct SignInRequest {
77 #[validate(email(message = "Invalid email address"))]
78 email: String,
79 #[validate(length(min = 1, message = "Password is required"))]
80 password: String,
81 #[serde(rename = "callbackURL")]
82 callback_url: Option<String>,
83 #[serde(rename = "rememberMe")]
84 remember_me: Option<bool>,
85}
86
87#[derive(Debug, Deserialize, Validate)]
88#[allow(dead_code)]
89pub(crate) struct SignInUsernameRequest {
90 #[validate(length(min = 1, message = "Username is required"))]
91 username: String,
92 #[validate(length(min = 1, message = "Password is required"))]
93 password: String,
94 #[serde(rename = "rememberMe")]
95 remember_me: Option<bool>,
96}
97
98#[derive(Debug, Serialize)]
99pub(crate) struct SignUpResponse<U: Serialize> {
100 token: Option<String>,
101 user: U,
102}
103
104#[derive(Debug, Serialize)]
105pub(crate) struct SignInResponse<U: Serialize> {
106 redirect: bool,
107 token: String,
108 url: Option<String>,
109 user: U,
110}
111
112#[derive(Debug, Serialize)]
114pub(crate) struct TwoFactorRedirectResponse {
115 #[serde(rename = "twoFactorRedirect")]
116 two_factor_redirect: bool,
117 token: String,
118}
119
120pub(crate) enum SignInCoreResult<U: Serialize> {
122 Success(SignInResponse<U>, String),
123 TwoFactorRedirect(TwoFactorRedirectResponse),
124}
125
126impl EmailPasswordPlugin {
127 #[allow(clippy::new_without_default)]
128 pub fn new() -> Self {
129 Self {
130 config: EmailPasswordConfig::default(),
131 email_verification: None,
132 }
133 }
134
135 pub fn with_config(config: EmailPasswordConfig) -> Self {
136 Self {
137 config,
138 email_verification: None,
139 }
140 }
141
142 pub fn with_email_verification(mut self, plugin: Arc<EmailVerificationPlugin>) -> Self {
145 self.email_verification = Some(plugin);
146 self
147 }
148
149 pub fn enable_signup(mut self, enable: bool) -> Self {
150 self.config.enable_signup = enable;
151 self
152 }
153
154 pub fn require_email_verification(mut self, require: bool) -> Self {
155 self.config.require_email_verification = require;
156 self
157 }
158
159 pub fn password_min_length(mut self, length: usize) -> Self {
160 self.config.password_min_length = length;
161 self
162 }
163
164 pub fn password_max_length(mut self, length: usize) -> Self {
165 self.config.password_max_length = length;
166 self
167 }
168
169 pub fn auto_sign_in(mut self, auto: bool) -> Self {
170 self.config.auto_sign_in = auto;
171 self
172 }
173
174 pub fn password_hasher(mut self, hasher: Arc<dyn PasswordHasher>) -> Self {
175 self.config.password_hasher = Some(hasher);
176 self
177 }
178
179 async fn handle_sign_up<DB: DatabaseAdapter>(
180 &self,
181 req: &AuthRequest,
182 ctx: &AuthContext<DB>,
183 ) -> AuthResult<AuthResponse> {
184 let signup_req: SignUpRequest = match better_auth_core::validate_request_body(req) {
185 Ok(v) => v,
186 Err(resp) => return Ok(resp),
187 };
188
189 let (response, session_token) = sign_up_core(&signup_req, &self.config, ctx).await?;
190
191 if let Some(token) = session_token {
192 let cookie_header = create_session_cookie(&token, &ctx.config);
193 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
194 } else {
195 Ok(AuthResponse::json(200, &response)?)
196 }
197 }
198
199 async fn handle_sign_in<DB: DatabaseAdapter>(
200 &self,
201 req: &AuthRequest,
202 ctx: &AuthContext<DB>,
203 ) -> AuthResult<AuthResponse> {
204 let signin_req: SignInRequest = match better_auth_core::validate_request_body(req) {
205 Ok(v) => v,
206 Err(resp) => return Ok(resp),
207 };
208
209 match sign_in_core(
210 &signin_req,
211 &self.config,
212 self.email_verification.as_deref(),
213 ctx,
214 )
215 .await?
216 {
217 SignInCoreResult::Success(response, token) => {
218 let cookie_header = create_session_cookie(&token, &ctx.config);
219 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
220 }
221 SignInCoreResult::TwoFactorRedirect(redirect) => {
222 Ok(AuthResponse::json(200, &redirect)?)
223 }
224 }
225 }
226
227 async fn handle_sign_in_username<DB: DatabaseAdapter>(
228 &self,
229 req: &AuthRequest,
230 ctx: &AuthContext<DB>,
231 ) -> AuthResult<AuthResponse> {
232 let signin_req: SignInUsernameRequest = match better_auth_core::validate_request_body(req) {
233 Ok(v) => v,
234 Err(resp) => return Ok(resp),
235 };
236
237 match sign_in_username_core(
238 &signin_req,
239 &self.config,
240 self.email_verification.as_deref(),
241 ctx,
242 )
243 .await?
244 {
245 SignInCoreResult::Success(response, token) => {
246 let cookie_header = create_session_cookie(&token, &ctx.config);
247 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
248 }
249 SignInCoreResult::TwoFactorRedirect(redirect) => {
250 Ok(AuthResponse::json(200, &redirect)?)
251 }
252 }
253 }
254}
255
256pub(crate) async fn sign_up_core<DB: DatabaseAdapter>(
265 body: &SignUpRequest,
266 config: &EmailPasswordConfig,
267 ctx: &AuthContext<DB>,
268) -> AuthResult<(SignUpResponse<DB::User>, Option<String>)> {
269 if !config.enable_signup {
270 return Err(AuthError::forbidden("User registration is not enabled"));
271 }
272
273 password_utils::validate_password(
274 &body.password,
275 config.password_min_length,
276 config.password_max_length,
277 ctx,
278 )?;
279
280 if ctx.database.get_user_by_email(&body.email).await?.is_some() {
282 return Err(AuthError::conflict("A user with this email already exists"));
283 }
284
285 let password_hash =
287 password_utils::hash_password(config.password_hasher.as_ref(), &body.password).await?;
288
289 let metadata = {
290 let mut m = serde_json::Map::new();
291 m.insert(
292 PASSWORD_HASH_KEY.to_string(),
293 serde_json::Value::String(password_hash),
294 );
295 serde_json::Value::Object(m)
296 };
297
298 let mut create_user = CreateUser::new()
299 .with_email(&body.email)
300 .with_name(&body.name);
301 if let Some(ref username) = body.username {
302 create_user = create_user.with_username(username.clone());
303 }
304 if let Some(ref display_username) = body.display_username {
305 create_user.display_username = Some(display_username.clone());
306 }
307 create_user.metadata = Some(metadata);
308
309 let user = ctx.database.create_user(create_user).await?;
310
311 if config.auto_sign_in {
312 let session = ctx
313 .session_manager()
314 .create_session(&user, None, None)
315 .await?;
316 let token = session.token().to_string();
317
318 let response = SignUpResponse {
319 token: Some(token.clone()),
320 user,
321 };
322 Ok((response, Some(token)))
323 } else {
324 let response = SignUpResponse { token: None, user };
325 Ok((response, None))
326 }
327}
328
329async fn sign_in_with_user_core<DB: DatabaseAdapter>(
331 user: DB::User,
332 password: &str,
333 config: &EmailPasswordConfig,
334 email_verification: Option<&EmailVerificationPlugin>,
335 callback_url: Option<&str>,
336 ctx: &AuthContext<DB>,
337) -> AuthResult<SignInCoreResult<DB::User>> {
338 let stored_hash = user.password_hash().ok_or(AuthError::InvalidCredentials)?;
340
341 password_utils::verify_password(config.password_hasher.as_ref(), password, stored_hash).await?;
342
343 if user.two_factor_enabled() {
345 let pending_token = format!("2fa_{}", uuid::Uuid::new_v4());
346 ctx.database
347 .create_verification(CreateVerification {
348 identifier: format!("2fa_pending:{}", pending_token),
349 value: user.id().to_string(),
350 expires_at: chrono::Utc::now() + chrono::Duration::minutes(5),
351 })
352 .await?;
353 return Ok(SignInCoreResult::TwoFactorRedirect(
354 TwoFactorRedirectResponse {
355 two_factor_redirect: true,
356 token: pending_token,
357 },
358 ));
359 }
360
361 if let Some(ev) = email_verification
363 && let Err(e) = ev
364 .send_verification_on_sign_in(&user, callback_url, ctx)
365 .await
366 {
367 tracing::warn!(
368 error = %e,
369 "Failed to send verification email on sign-in"
370 );
371 }
372
373 let session = ctx
375 .session_manager()
376 .create_session(&user, None, None)
377 .await?;
378 let token = session.token().to_string();
379
380 let response = SignInResponse {
381 redirect: false,
382 token: token.clone(),
383 url: None,
384 user,
385 };
386 Ok(SignInCoreResult::Success(response, token))
387}
388
389pub(crate) async fn sign_in_core<DB: DatabaseAdapter>(
391 body: &SignInRequest,
392 config: &EmailPasswordConfig,
393 email_verification: Option<&EmailVerificationPlugin>,
394 ctx: &AuthContext<DB>,
395) -> AuthResult<SignInCoreResult<DB::User>> {
396 let user = ctx
397 .database
398 .get_user_by_email(&body.email)
399 .await?
400 .ok_or(AuthError::InvalidCredentials)?;
401
402 sign_in_with_user_core(
403 user,
404 &body.password,
405 config,
406 email_verification,
407 body.callback_url.as_deref(),
408 ctx,
409 )
410 .await
411}
412
413pub(crate) async fn sign_in_username_core<DB: DatabaseAdapter>(
415 body: &SignInUsernameRequest,
416 config: &EmailPasswordConfig,
417 email_verification: Option<&EmailVerificationPlugin>,
418 ctx: &AuthContext<DB>,
419) -> AuthResult<SignInCoreResult<DB::User>> {
420 let user = ctx
421 .database
422 .get_user_by_username(&body.username)
423 .await?
424 .ok_or(AuthError::InvalidCredentials)?;
425
426 sign_in_with_user_core(user, &body.password, config, email_verification, None, ctx).await
427}
428
429impl Default for EmailPasswordConfig {
430 fn default() -> Self {
431 Self {
432 enable_signup: true,
433 require_email_verification: false,
434 password_min_length: 8,
435 password_max_length: 128,
436 auto_sign_in: true,
437 password_hasher: None,
438 }
439 }
440}
441
442#[async_trait]
443impl<DB: DatabaseAdapter> AuthPlugin<DB> for EmailPasswordPlugin {
444 fn name(&self) -> &'static str {
445 "email-password"
446 }
447
448 fn routes(&self) -> Vec<AuthRoute> {
449 let mut routes = vec![
450 AuthRoute::post("/sign-in/email", "sign_in_email"),
451 AuthRoute::post("/sign-in/username", "sign_in_username"),
452 ];
453
454 if self.config.enable_signup {
455 routes.push(AuthRoute::post("/sign-up/email", "sign_up_email"));
456 }
457
458 routes
459 }
460
461 async fn on_request(
462 &self,
463 req: &AuthRequest,
464 ctx: &AuthContext<DB>,
465 ) -> AuthResult<Option<AuthResponse>> {
466 match (req.method(), req.path()) {
467 (HttpMethod::Post, "/sign-up/email") if self.config.enable_signup => {
468 Ok(Some(self.handle_sign_up(req, ctx).await?))
469 }
470 (HttpMethod::Post, "/sign-in/email") => Ok(Some(self.handle_sign_in(req, ctx).await?)),
471 (HttpMethod::Post, "/sign-in/username") => {
472 Ok(Some(self.handle_sign_in_username(req, ctx).await?))
473 }
474 _ => Ok(None),
475 }
476 }
477
478 async fn on_user_created(&self, user: &DB::User, _ctx: &AuthContext<DB>) -> AuthResult<()> {
479 if self.config.require_email_verification
480 && !user.email_verified()
481 && let Some(email) = user.email()
482 {
483 println!("Email verification required for user: {}", email);
484 }
485 Ok(())
486 }
487}
488
489#[cfg(feature = "axum")]
490mod axum_impl {
491 use super::*;
492 use std::sync::Arc;
493
494 use axum::Json;
495 use axum::extract::{Extension, State};
496 use axum::http::header;
497 use axum::response::IntoResponse;
498 use better_auth_core::{AuthState, ValidatedJson};
499
500 type SharedPlugin = Arc<EmailPasswordPlugin>;
503
504 async fn handle_sign_up<DB: DatabaseAdapter>(
505 State(state): State<AuthState<DB>>,
506 Extension(plugin): Extension<SharedPlugin>,
507 ValidatedJson(body): ValidatedJson<SignUpRequest>,
508 ) -> Result<axum::response::Response, AuthError> {
509 let ctx = state.to_context();
510 let (response, session_token) = sign_up_core(&body, &plugin.config, &ctx).await?;
511
512 if let Some(token) = session_token {
513 let cookie = state.session_cookie(&token);
514 Ok(([(header::SET_COOKIE, cookie)], Json(response)).into_response())
515 } else {
516 Ok(Json(response).into_response())
517 }
518 }
519
520 fn sign_in_result_to_response<DB: DatabaseAdapter>(
522 result: SignInCoreResult<DB::User>,
523 state: &AuthState<DB>,
524 ) -> axum::response::Response {
525 match result {
526 SignInCoreResult::Success(response, token) => {
527 let cookie = state.session_cookie(&token);
528 ([(header::SET_COOKIE, cookie)], Json(response)).into_response()
529 }
530 SignInCoreResult::TwoFactorRedirect(redirect) => Json(redirect).into_response(),
531 }
532 }
533
534 async fn handle_sign_in<DB: DatabaseAdapter>(
535 State(state): State<AuthState<DB>>,
536 Extension(plugin): Extension<SharedPlugin>,
537 ValidatedJson(body): ValidatedJson<SignInRequest>,
538 ) -> Result<axum::response::Response, AuthError> {
539 let ctx = state.to_context();
540 let result = sign_in_core(
541 &body,
542 &plugin.config,
543 plugin.email_verification.as_deref(),
544 &ctx,
545 )
546 .await?;
547 Ok(sign_in_result_to_response::<DB>(result, &state))
548 }
549
550 async fn handle_sign_in_username<DB: DatabaseAdapter>(
551 State(state): State<AuthState<DB>>,
552 Extension(plugin): Extension<SharedPlugin>,
553 ValidatedJson(body): ValidatedJson<SignInUsernameRequest>,
554 ) -> Result<axum::response::Response, AuthError> {
555 let ctx = state.to_context();
556 let result = sign_in_username_core(
557 &body,
558 &plugin.config,
559 plugin.email_verification.as_deref(),
560 &ctx,
561 )
562 .await?;
563 Ok(sign_in_result_to_response::<DB>(result, &state))
564 }
565
566 #[async_trait::async_trait]
567 impl<DB: DatabaseAdapter> better_auth_core::AxumPlugin<DB> for EmailPasswordPlugin {
568 fn name(&self) -> &'static str {
569 "email-password"
570 }
571
572 fn router(&self) -> axum::Router<AuthState<DB>> {
573 use axum::routing::post;
574
575 let shared: SharedPlugin = Arc::new(EmailPasswordPlugin {
576 config: self.config.clone(),
577 email_verification: self.email_verification.clone(),
578 });
579
580 axum::Router::new()
581 .route("/sign-up/email", post(handle_sign_up::<DB>))
582 .route("/sign-in/email", post(handle_sign_in::<DB>))
583 .route("/sign-in/username", post(handle_sign_in_username::<DB>))
584 .layer(Extension(shared))
585 }
586
587 async fn on_user_created(
588 &self,
589 user: &DB::User,
590 _ctx: &better_auth_core::AuthContext<DB>,
591 ) -> better_auth_core::AuthResult<()> {
592 if self.config.require_email_verification
593 && !user.email_verified()
594 && let Some(email) = user.email()
595 {
596 println!("Email verification required for user: {}", email);
597 }
598 Ok(())
599 }
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use better_auth_core::AuthContext;
607 use better_auth_core::adapters::{MemoryDatabaseAdapter, UserOps};
608 use better_auth_core::config::AuthConfig;
609 use std::collections::HashMap;
610 use std::sync::Arc;
611
612 fn create_test_context() -> AuthContext<MemoryDatabaseAdapter> {
613 let config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
614 let config = Arc::new(config);
615 let database = Arc::new(MemoryDatabaseAdapter::new());
616 AuthContext::new(config, database)
617 }
618
619 fn create_signup_request(email: &str, password: &str) -> AuthRequest {
620 let body = serde_json::json!({
621 "name": "Test User",
622 "email": email,
623 "password": password,
624 });
625 AuthRequest::from_parts(
626 HttpMethod::Post,
627 "/sign-up/email".to_string(),
628 HashMap::new(),
629 Some(body.to_string().into_bytes()),
630 HashMap::new(),
631 )
632 }
633
634 #[tokio::test]
635 async fn test_auto_sign_in_false_returns_no_session() {
636 let plugin = EmailPasswordPlugin::new().auto_sign_in(false);
637 let ctx = create_test_context();
638
639 let req = create_signup_request("auto@example.com", "Password123!");
640 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
641 assert_eq!(response.status, 200);
642
643 let has_cookie = response
645 .headers
646 .iter()
647 .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
648 assert!(!has_cookie, "auto_sign_in=false should not set a cookie");
649
650 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
652 assert!(
653 body["token"].is_null(),
654 "auto_sign_in=false should return null token"
655 );
656 assert!(body["user"]["id"].is_string());
658 }
659
660 #[tokio::test]
661 async fn test_auto_sign_in_true_returns_session() {
662 let plugin = EmailPasswordPlugin::new(); let ctx = create_test_context();
664
665 let req = create_signup_request("autotrue@example.com", "Password123!");
666 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
667 assert_eq!(response.status, 200);
668
669 let has_cookie = response
671 .headers
672 .iter()
673 .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
674 assert!(has_cookie, "auto_sign_in=true should set a cookie");
675
676 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
678 assert!(
679 body["token"].is_string(),
680 "auto_sign_in=true should return a session token"
681 );
682 }
683
684 #[tokio::test]
685 async fn test_password_max_length_rejection() {
686 let plugin = EmailPasswordPlugin::new().password_max_length(128);
687 let ctx = create_test_context();
688
689 let long_password = format!("A1!{}", "a".repeat(126)); let req = create_signup_request("long@example.com", &long_password);
692 let err = plugin.handle_sign_up(&req, &ctx).await.unwrap_err();
693 assert_eq!(err.status_code(), 400);
694
695 let ok_password = format!("A1!{}", "a".repeat(125)); let req = create_signup_request("ok@example.com", &ok_password);
698 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
699 assert_eq!(response.status, 200);
700 }
701
702 #[tokio::test]
703 async fn test_custom_password_hasher() {
704 struct TestHasher;
706
707 #[async_trait]
708 impl PasswordHasher for TestHasher {
709 async fn hash(&self, password: &str) -> AuthResult<String> {
710 Ok(format!("hashed:{}", password))
711 }
712 async fn verify(&self, hash: &str, password: &str) -> AuthResult<bool> {
713 Ok(hash == format!("hashed:{}", password))
714 }
715 }
716
717 let hasher: Arc<dyn PasswordHasher> = Arc::new(TestHasher);
718 let plugin = EmailPasswordPlugin::new().password_hasher(hasher);
719 let ctx = create_test_context();
720
721 let req = create_signup_request("hasher@example.com", "Password123!");
723 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
724 assert_eq!(response.status, 200);
725
726 let user = ctx
728 .database
729 .get_user_by_email("hasher@example.com")
730 .await
731 .unwrap()
732 .unwrap();
733 let stored_hash = user
734 .metadata
735 .get(PASSWORD_HASH_KEY)
736 .unwrap()
737 .as_str()
738 .unwrap();
739 assert_eq!(stored_hash, "hashed:Password123!");
740
741 let signin_body = serde_json::json!({
743 "email": "hasher@example.com",
744 "password": "Password123!",
745 });
746 let signin_req = AuthRequest::from_parts(
747 HttpMethod::Post,
748 "/sign-in/email".to_string(),
749 HashMap::new(),
750 Some(signin_body.to_string().into_bytes()),
751 HashMap::new(),
752 );
753 let response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
754 assert_eq!(response.status, 200);
755
756 let bad_body = serde_json::json!({
758 "email": "hasher@example.com",
759 "password": "WrongPassword!",
760 });
761 let bad_req = AuthRequest::from_parts(
762 HttpMethod::Post,
763 "/sign-in/email".to_string(),
764 HashMap::new(),
765 Some(bad_body.to_string().into_bytes()),
766 HashMap::new(),
767 );
768 let err = plugin.handle_sign_in(&bad_req, &ctx).await.unwrap_err();
769 assert_eq!(err.to_string(), AuthError::InvalidCredentials.to_string());
770 }
771}