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::{AuthRequest, AuthResponse, CreateUser, CreateVerification, HttpMethod};
11
12use super::email_verification::EmailVerificationPlugin;
13use better_auth_core::utils::cookie_utils::create_session_cookie;
14use better_auth_core::utils::password::{self as password_utils, PasswordHasher};
15pub struct EmailPasswordPlugin {
17 config: EmailPasswordConfig,
18 email_verification: Option<Arc<EmailVerificationPlugin>>,
21}
22
23#[derive(Clone)]
24pub struct EmailPasswordConfig {
25 pub enable_signup: bool,
26 pub require_email_verification: bool,
27 pub password_min_length: usize,
28 pub password_max_length: usize,
30 pub auto_sign_in: bool,
33 pub password_hasher: Option<Arc<dyn PasswordHasher>>,
35}
36
37impl std::fmt::Debug for EmailPasswordConfig {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("EmailPasswordConfig")
40 .field("enable_signup", &self.enable_signup)
41 .field(
42 "require_email_verification",
43 &self.require_email_verification,
44 )
45 .field("password_min_length", &self.password_min_length)
46 .field("password_max_length", &self.password_max_length)
47 .field("auto_sign_in", &self.auto_sign_in)
48 .field(
49 "password_hasher",
50 &self.password_hasher.as_ref().map(|_| "custom"),
51 )
52 .finish()
53 }
54}
55
56#[derive(Debug, Deserialize, Validate)]
57#[allow(dead_code)]
58struct SignUpRequest {
59 #[validate(length(min = 1, message = "Name is required"))]
60 name: String,
61 #[validate(email(message = "Invalid email address"))]
62 email: String,
63 #[validate(length(min = 1, message = "Password is required"))]
64 password: String,
65 username: Option<String>,
66 #[serde(rename = "displayUsername")]
67 display_username: Option<String>,
68 #[serde(rename = "callbackURL")]
69 callback_url: Option<String>,
70}
71
72#[derive(Debug, Deserialize, Validate)]
73#[allow(dead_code)]
74struct SignInRequest {
75 #[validate(email(message = "Invalid email address"))]
76 email: String,
77 #[validate(length(min = 1, message = "Password is required"))]
78 password: String,
79 #[serde(rename = "callbackURL")]
80 callback_url: Option<String>,
81 #[serde(rename = "rememberMe")]
82 remember_me: Option<bool>,
83}
84
85#[derive(Debug, Deserialize, Validate)]
86#[allow(dead_code)]
87struct SignInUsernameRequest {
88 #[validate(length(min = 1, message = "Username is required"))]
89 username: String,
90 #[validate(length(min = 1, message = "Password is required"))]
91 password: String,
92 #[serde(rename = "rememberMe")]
93 remember_me: Option<bool>,
94}
95
96#[derive(Debug, Serialize)]
97struct SignUpResponse<U: Serialize> {
98 token: Option<String>,
99 user: U,
100}
101
102#[derive(Debug, Serialize)]
103struct SignInResponse<U: Serialize> {
104 redirect: bool,
105 token: String,
106 url: Option<String>,
107 user: U,
108}
109
110impl EmailPasswordPlugin {
111 #[allow(clippy::new_without_default)]
112 pub fn new() -> Self {
113 Self {
114 config: EmailPasswordConfig::default(),
115 email_verification: None,
116 }
117 }
118
119 pub fn with_config(config: EmailPasswordConfig) -> Self {
120 Self {
121 config,
122 email_verification: None,
123 }
124 }
125
126 pub fn with_email_verification(mut self, plugin: Arc<EmailVerificationPlugin>) -> Self {
129 self.email_verification = Some(plugin);
130 self
131 }
132
133 pub fn enable_signup(mut self, enable: bool) -> Self {
134 self.config.enable_signup = enable;
135 self
136 }
137
138 pub fn require_email_verification(mut self, require: bool) -> Self {
139 self.config.require_email_verification = require;
140 self
141 }
142
143 pub fn password_min_length(mut self, length: usize) -> Self {
144 self.config.password_min_length = length;
145 self
146 }
147
148 pub fn password_max_length(mut self, length: usize) -> Self {
149 self.config.password_max_length = length;
150 self
151 }
152
153 pub fn auto_sign_in(mut self, auto: bool) -> Self {
154 self.config.auto_sign_in = auto;
155 self
156 }
157
158 pub fn password_hasher(mut self, hasher: Arc<dyn PasswordHasher>) -> Self {
159 self.config.password_hasher = Some(hasher);
160 self
161 }
162
163 async fn handle_sign_up<DB: DatabaseAdapter>(
164 &self,
165 req: &AuthRequest,
166 ctx: &AuthContext<DB>,
167 ) -> AuthResult<AuthResponse> {
168 if !self.config.enable_signup {
169 return Err(AuthError::forbidden("User registration is not enabled"));
170 }
171
172 let signup_req: SignUpRequest = match better_auth_core::validate_request_body(req) {
173 Ok(v) => v,
174 Err(resp) => return Ok(resp),
175 };
176
177 self.validate_password(&signup_req.password, ctx)?;
179
180 if ctx
182 .database
183 .get_user_by_email(&signup_req.email)
184 .await?
185 .is_some()
186 {
187 return Err(AuthError::conflict("A user with this email already exists"));
188 }
189
190 let password_hash = self.hash_password(&signup_req.password).await?;
192
193 let metadata = serde_json::json!({
195 "password_hash": password_hash,
196 });
197
198 let mut create_user = CreateUser::new()
199 .with_email(&signup_req.email)
200 .with_name(&signup_req.name);
201 if let Some(username) = signup_req.username {
202 create_user = create_user.with_username(username);
203 }
204 if let Some(display_username) = signup_req.display_username {
205 create_user.display_username = Some(display_username);
206 }
207 create_user.metadata = Some(metadata);
208
209 let user = ctx.database.create_user(create_user).await?;
210
211 if self.config.auto_sign_in {
212 let session_manager =
214 better_auth_core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
215 let session = session_manager.create_session(&user, None, None).await?;
216
217 let response = SignUpResponse {
218 token: Some(session.token().to_string()),
219 user,
220 };
221
222 let cookie_header = create_session_cookie(session.token(), ctx);
224
225 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
226 } else {
227 let response = SignUpResponse { token: None, user };
228
229 Ok(AuthResponse::json(200, &response)?)
230 }
231 }
232
233 async fn handle_sign_in<DB: DatabaseAdapter>(
234 &self,
235 req: &AuthRequest,
236 ctx: &AuthContext<DB>,
237 ) -> AuthResult<AuthResponse> {
238 let signin_req: SignInRequest = match better_auth_core::validate_request_body(req) {
239 Ok(v) => v,
240 Err(resp) => return Ok(resp),
241 };
242
243 let user = ctx
245 .database
246 .get_user_by_email(&signin_req.email)
247 .await?
248 .ok_or(AuthError::InvalidCredentials)?;
249
250 let stored_hash = user
252 .metadata()
253 .get("password_hash")
254 .and_then(|v| v.as_str())
255 .ok_or(AuthError::InvalidCredentials)?;
256
257 self.verify_password(&signin_req.password, stored_hash)
258 .await?;
259
260 if user.two_factor_enabled() {
262 let pending_token = format!("2fa_{}", uuid::Uuid::new_v4());
263 ctx.database
264 .create_verification(CreateVerification {
265 identifier: format!("2fa_pending:{}", pending_token),
266 value: user.id().to_string(),
267 expires_at: chrono::Utc::now() + chrono::Duration::minutes(5),
268 })
269 .await?;
270 return Ok(AuthResponse::json(
271 200,
272 &serde_json::json!({
273 "twoFactorRedirect": true,
274 "token": pending_token,
275 }),
276 )?);
277 }
278
279 if let Some(ref ev) = self.email_verification
281 && let Err(e) = ev
282 .send_verification_on_sign_in(&user, signin_req.callback_url.as_deref(), ctx)
283 .await
284 {
285 tracing::warn!(
286 error = %e,
287 "Failed to send verification email on sign-in"
288 );
289 }
290
291 let session_manager =
293 better_auth_core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
294 let session = session_manager.create_session(&user, None, None).await?;
295
296 let response = SignInResponse {
297 redirect: false,
298 token: session.token().to_string(),
299 url: None,
300 user,
301 };
302
303 let cookie_header = create_session_cookie(session.token(), ctx);
305
306 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
307 }
308
309 async fn handle_sign_in_username<DB: DatabaseAdapter>(
310 &self,
311 req: &AuthRequest,
312 ctx: &AuthContext<DB>,
313 ) -> AuthResult<AuthResponse> {
314 let signin_req: SignInUsernameRequest = match better_auth_core::validate_request_body(req) {
315 Ok(v) => v,
316 Err(resp) => return Ok(resp),
317 };
318
319 let user = ctx
321 .database
322 .get_user_by_username(&signin_req.username)
323 .await?
324 .ok_or(AuthError::InvalidCredentials)?;
325
326 let stored_hash = user
328 .metadata()
329 .get("password_hash")
330 .and_then(|v| v.as_str())
331 .ok_or(AuthError::InvalidCredentials)?;
332
333 self.verify_password(&signin_req.password, stored_hash)
334 .await?;
335
336 if user.two_factor_enabled() {
338 let pending_token = format!("2fa_{}", uuid::Uuid::new_v4());
339 ctx.database
340 .create_verification(CreateVerification {
341 identifier: format!("2fa_pending:{}", pending_token),
342 value: user.id().to_string(),
343 expires_at: chrono::Utc::now() + chrono::Duration::minutes(5),
344 })
345 .await?;
346 return Ok(AuthResponse::json(
347 200,
348 &serde_json::json!({
349 "twoFactorRedirect": true,
350 "token": pending_token,
351 }),
352 )?);
353 }
354
355 if let Some(ref ev) = self.email_verification
357 && let Err(e) = ev.send_verification_on_sign_in(&user, None, ctx).await
358 {
359 tracing::warn!(
360 error = %e,
361 "Failed to send verification email on sign-in"
362 );
363 }
364
365 let session_manager =
367 better_auth_core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
368 let session = session_manager.create_session(&user, None, None).await?;
369
370 let response = SignInResponse {
371 redirect: false,
372 token: session.token().to_string(),
373 url: None,
374 user,
375 };
376
377 let cookie_header = create_session_cookie(session.token(), ctx);
379
380 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
381 }
382
383 fn validate_password<DB: DatabaseAdapter>(
384 &self,
385 password: &str,
386 ctx: &AuthContext<DB>,
387 ) -> AuthResult<()> {
388 password_utils::validate_password(
389 password,
390 self.config.password_min_length,
391 self.config.password_max_length,
392 ctx,
393 )
394 }
395
396 async fn hash_password(&self, password: &str) -> AuthResult<String> {
397 password_utils::hash_password(self.config.password_hasher.as_ref(), password).await
398 }
399
400 async fn verify_password(&self, password: &str, hash: &str) -> AuthResult<()> {
401 password_utils::verify_password(self.config.password_hasher.as_ref(), password, hash).await
402 }
403}
404
405impl Default for EmailPasswordConfig {
406 fn default() -> Self {
407 Self {
408 enable_signup: true,
409 require_email_verification: false,
410 password_min_length: 8,
411 password_max_length: 128,
412 auto_sign_in: true,
413 password_hasher: None,
414 }
415 }
416}
417
418#[async_trait]
419impl<DB: DatabaseAdapter> AuthPlugin<DB> for EmailPasswordPlugin {
420 fn name(&self) -> &'static str {
421 "email-password"
422 }
423
424 fn routes(&self) -> Vec<AuthRoute> {
425 let mut routes = vec![
426 AuthRoute::post("/sign-in/email", "sign_in_email"),
427 AuthRoute::post("/sign-in/username", "sign_in_username"),
428 ];
429
430 if self.config.enable_signup {
431 routes.push(AuthRoute::post("/sign-up/email", "sign_up_email"));
432 }
433
434 routes
435 }
436
437 async fn on_request(
438 &self,
439 req: &AuthRequest,
440 ctx: &AuthContext<DB>,
441 ) -> AuthResult<Option<AuthResponse>> {
442 match (req.method(), req.path()) {
443 (HttpMethod::Post, "/sign-up/email") if self.config.enable_signup => {
444 Ok(Some(self.handle_sign_up(req, ctx).await?))
445 }
446 (HttpMethod::Post, "/sign-in/email") => Ok(Some(self.handle_sign_in(req, ctx).await?)),
447 (HttpMethod::Post, "/sign-in/username") => {
448 Ok(Some(self.handle_sign_in_username(req, ctx).await?))
449 }
450 _ => Ok(None),
451 }
452 }
453
454 async fn on_user_created(&self, user: &DB::User, _ctx: &AuthContext<DB>) -> AuthResult<()> {
455 if self.config.require_email_verification
456 && !user.email_verified()
457 && let Some(email) = user.email()
458 {
459 println!("Email verification required for user: {}", email);
460 }
461 Ok(())
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use better_auth_core::AuthContext;
469 use better_auth_core::adapters::{MemoryDatabaseAdapter, UserOps};
470 use better_auth_core::config::AuthConfig;
471 use std::collections::HashMap;
472 use std::sync::Arc;
473
474 fn create_test_context() -> AuthContext<MemoryDatabaseAdapter> {
475 let config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
476 let config = Arc::new(config);
477 let database = Arc::new(MemoryDatabaseAdapter::new());
478 AuthContext::new(config, database)
479 }
480
481 fn create_signup_request(email: &str, password: &str) -> AuthRequest {
482 let body = serde_json::json!({
483 "name": "Test User",
484 "email": email,
485 "password": password,
486 });
487 AuthRequest::from_parts(
488 HttpMethod::Post,
489 "/sign-up/email".to_string(),
490 HashMap::new(),
491 Some(body.to_string().into_bytes()),
492 HashMap::new(),
493 )
494 }
495
496 #[tokio::test]
497 async fn test_auto_sign_in_false_returns_no_session() {
498 let plugin = EmailPasswordPlugin::new().auto_sign_in(false);
499 let ctx = create_test_context();
500
501 let req = create_signup_request("auto@example.com", "Password123!");
502 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
503 assert_eq!(response.status, 200);
504
505 let has_cookie = response
507 .headers
508 .iter()
509 .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
510 assert!(!has_cookie, "auto_sign_in=false should not set a cookie");
511
512 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
514 assert!(
515 body["token"].is_null(),
516 "auto_sign_in=false should return null token"
517 );
518 assert!(body["user"]["id"].is_string());
520 }
521
522 #[tokio::test]
523 async fn test_auto_sign_in_true_returns_session() {
524 let plugin = EmailPasswordPlugin::new(); let ctx = create_test_context();
526
527 let req = create_signup_request("autotrue@example.com", "Password123!");
528 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
529 assert_eq!(response.status, 200);
530
531 let has_cookie = response
533 .headers
534 .iter()
535 .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
536 assert!(has_cookie, "auto_sign_in=true should set a cookie");
537
538 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
540 assert!(
541 body["token"].is_string(),
542 "auto_sign_in=true should return a session token"
543 );
544 }
545
546 #[tokio::test]
547 async fn test_password_max_length_rejection() {
548 let plugin = EmailPasswordPlugin::new().password_max_length(128);
549 let ctx = create_test_context();
550
551 let long_password = format!("A1!{}", "a".repeat(126)); let req = create_signup_request("long@example.com", &long_password);
554 let err = plugin.handle_sign_up(&req, &ctx).await.unwrap_err();
555 assert_eq!(err.status_code(), 400);
556
557 let ok_password = format!("A1!{}", "a".repeat(125)); let req = create_signup_request("ok@example.com", &ok_password);
560 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
561 assert_eq!(response.status, 200);
562 }
563
564 #[tokio::test]
565 async fn test_custom_password_hasher() {
566 struct TestHasher;
568
569 #[async_trait]
570 impl PasswordHasher for TestHasher {
571 async fn hash(&self, password: &str) -> AuthResult<String> {
572 Ok(format!("hashed:{}", password))
573 }
574 async fn verify(&self, hash: &str, password: &str) -> AuthResult<bool> {
575 Ok(hash == format!("hashed:{}", password))
576 }
577 }
578
579 let hasher: Arc<dyn PasswordHasher> = Arc::new(TestHasher);
580 let plugin = EmailPasswordPlugin::new().password_hasher(hasher);
581 let ctx = create_test_context();
582
583 let req = create_signup_request("hasher@example.com", "Password123!");
585 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
586 assert_eq!(response.status, 200);
587
588 let user = ctx
590 .database
591 .get_user_by_email("hasher@example.com")
592 .await
593 .unwrap()
594 .unwrap();
595 let stored_hash = user
596 .metadata
597 .get("password_hash")
598 .unwrap()
599 .as_str()
600 .unwrap();
601 assert_eq!(stored_hash, "hashed:Password123!");
602
603 let signin_body = serde_json::json!({
605 "email": "hasher@example.com",
606 "password": "Password123!",
607 });
608 let signin_req = AuthRequest::from_parts(
609 HttpMethod::Post,
610 "/sign-in/email".to_string(),
611 HashMap::new(),
612 Some(signin_body.to_string().into_bytes()),
613 HashMap::new(),
614 );
615 let response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
616 assert_eq!(response.status, 200);
617
618 let bad_body = serde_json::json!({
620 "email": "hasher@example.com",
621 "password": "WrongPassword!",
622 });
623 let bad_req = AuthRequest::from_parts(
624 HttpMethod::Post,
625 "/sign-in/email".to_string(),
626 HashMap::new(),
627 Some(bad_body.to_string().into_bytes()),
628 HashMap::new(),
629 );
630 let err = plugin.handle_sign_in(&bad_req, &ctx).await.unwrap_err();
631 assert_eq!(err.to_string(), AuthError::InvalidCredentials.to_string());
632 }
633}