1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use std::sync::Arc;
4use validator::{Validate, ValidateEmail};
5
6use better_auth_core::entity::{AuthAccount, AuthSession, AuthUser};
7use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
8use better_auth_core::{AuthError, AuthResult};
9use better_auth_core::{
10 AuthRequest, AuthResponse, CreateAccount, CreateSession, CreateUser, ErrorCodeMessageResponse,
11 HttpMethod, RequestMeta,
12};
13
14use super::{email_verification::EmailVerificationPlugin, two_factor};
15use better_auth_core::utils::cookie_utils::{
16 create_session_cookie, create_session_cookie_with_max_age,
17};
18use better_auth_core::utils::password::{self as password_utils, PasswordHasher};
19use better_auth_core::utils::username::{
20 UsernameValidationError, normalize_username, normalize_username_fields, validate_username,
21};
22use better_auth_core::wire::UserView;
23
24use crate::plugins::helpers::{SessionIssueError, apply_default_role, issue_user_session};
25
26const MESSAGE_INVALID_USERNAME_OR_PASSWORD: &str = "Invalid username or password";
27const MESSAGE_EMAIL_NOT_VERIFIED: &str = "Email not verified";
28const MESSAGE_USERNAME_TOO_SHORT: &str = "Username is too short";
29const MESSAGE_USERNAME_TOO_LONG: &str = "Username is too long";
30const MESSAGE_INVALID_USERNAME: &str = "Username is invalid";
31const MESSAGE_USERNAME_IS_ALREADY_TAKEN: &str = "Username is already taken. Please try another.";
32
33fn username_error_response(status: u16, code: &str, message: &str) -> AuthResult<AuthResponse> {
34 AuthResponse::json(
35 status,
36 &ErrorCodeMessageResponse {
37 code: Some(code.to_string()),
38 message: message.to_string(),
39 },
40 )
41 .map_err(AuthError::from)
42}
43
44fn create_session_cookie_for_remember_me(
45 token: &str,
46 remember_me: Option<bool>,
47 config: &better_auth_core::AuthConfig,
48) -> String {
49 if remember_me == Some(false) {
50 create_session_cookie_with_max_age(Some(token), None, config)
51 } else {
52 create_session_cookie(token, config)
53 }
54}
55pub struct EmailPasswordPlugin {
57 config: EmailPasswordConfig,
58 email_verification: Option<Arc<EmailVerificationPlugin>>,
61}
62
63#[derive(Clone)]
64pub struct EmailPasswordConfig {
65 pub enable_signup: bool,
66 pub require_email_verification: bool,
67 pub password_min_length: usize,
68 pub password_max_length: usize,
70 pub auto_sign_in: bool,
73 pub password_hasher: Option<Arc<dyn PasswordHasher>>,
75}
76
77impl std::fmt::Debug for EmailPasswordConfig {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("EmailPasswordConfig")
80 .field("enable_signup", &self.enable_signup)
81 .field(
82 "require_email_verification",
83 &self.require_email_verification,
84 )
85 .field("password_min_length", &self.password_min_length)
86 .field("password_max_length", &self.password_max_length)
87 .field("auto_sign_in", &self.auto_sign_in)
88 .field(
89 "password_hasher",
90 &self.password_hasher.as_ref().map(|_| "custom"),
91 )
92 .finish()
93 }
94}
95
96#[derive(Debug, Deserialize, Validate)]
97#[expect(dead_code, reason = "fields deserialized from request body")]
98pub(crate) struct SignUpRequest {
99 #[validate(length(min = 1, message = "Name is required"))]
100 name: String,
101 #[validate(email(message = "Invalid email address"))]
102 email: String,
103 #[validate(length(min = 1, message = "Password is required"))]
104 password: String,
105 username: Option<String>,
106 #[serde(rename = "displayUsername")]
107 display_username: Option<String>,
108 #[serde(rename = "callbackURL")]
109 callback_url: Option<String>,
110}
111
112#[derive(Debug, Deserialize, Validate)]
113pub(crate) struct SignInRequest {
114 #[validate(email(message = "Invalid email address"))]
115 email: String,
116 #[validate(length(min = 1, message = "Password is required"))]
117 password: String,
118 #[serde(rename = "callbackURL")]
119 callback_url: Option<String>,
120 #[serde(rename = "rememberMe")]
121 remember_me: Option<bool>,
122}
123
124#[derive(Debug, Deserialize, Validate)]
125pub(crate) struct SignInUsernameRequest {
126 username: String,
127 password: String,
128 #[serde(rename = "rememberMe")]
129 remember_me: Option<bool>,
130 #[serde(rename = "callbackURL")]
131 callback_url: Option<String>,
132}
133
134#[derive(Debug, Deserialize, Validate)]
135struct IsUsernameAvailableRequest {
136 username: String,
137}
138
139#[derive(Debug, Serialize)]
140struct IsUsernameAvailableResponse {
141 available: bool,
142}
143
144#[derive(Debug, Serialize)]
145pub(crate) struct SignUpResponse<U: Serialize> {
146 token: Option<String>,
147 user: U,
148}
149
150#[derive(Debug, Serialize)]
151pub(crate) struct SignInResponse<U: Serialize> {
152 redirect: bool,
153 token: String,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 url: Option<String>,
156 user: U,
157}
158
159#[derive(Debug, Serialize)]
160pub(crate) struct SignInUsernameResponse<U: Serialize> {
161 redirect: bool,
163 token: String,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 url: Option<String>,
166 user: U,
167}
168
169pub(crate) enum SignInCoreResult<U: Serialize> {
171 Success {
172 response: SignInResponse<U>,
173 token: String,
174 set_cookie_headers: Vec<String>,
175 },
176 TwoFactorRedirect {
177 response: two_factor::TwoFactorRedirectResponse,
178 set_cookie_headers: Vec<String>,
179 },
180}
181
182impl EmailPasswordPlugin {
183 #[expect(
184 clippy::new_without_default,
185 reason = "plugin construction is intentionally explicit"
186 )]
187 pub fn new() -> Self {
188 Self {
189 config: EmailPasswordConfig::default(),
190 email_verification: None,
191 }
192 }
193
194 pub fn with_config(config: EmailPasswordConfig) -> Self {
195 Self {
196 config,
197 email_verification: None,
198 }
199 }
200
201 pub fn with_email_verification(mut self, plugin: Arc<EmailVerificationPlugin>) -> Self {
204 self.email_verification = Some(plugin);
205 self
206 }
207
208 pub fn enable_signup(mut self, enable: bool) -> Self {
209 self.config.enable_signup = enable;
210 self
211 }
212
213 pub fn require_email_verification(mut self, require: bool) -> Self {
214 self.config.require_email_verification = require;
215 self
216 }
217
218 pub fn password_min_length(mut self, length: usize) -> Self {
219 self.config.password_min_length = length;
220 self
221 }
222
223 pub fn password_max_length(mut self, length: usize) -> Self {
224 self.config.password_max_length = length;
225 self
226 }
227
228 pub fn auto_sign_in(mut self, auto: bool) -> Self {
229 self.config.auto_sign_in = auto;
230 self
231 }
232
233 pub fn password_hasher(mut self, hasher: Arc<dyn PasswordHasher>) -> Self {
234 self.config.password_hasher = Some(hasher);
235 self
236 }
237
238 async fn handle_sign_up(
239 &self,
240 req: &AuthRequest,
241 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
242 ) -> AuthResult<AuthResponse> {
243 let mut signup_req: SignUpRequest = match better_auth_core::validate_request_body(req) {
244 Ok(v) => v,
245 Err(resp) => return Ok(resp),
246 };
247
248 let (username, display_username) = normalize_username_fields(
249 signup_req.username.take(),
250 signup_req.display_username.take(),
251 );
252 signup_req.username = username;
253 signup_req.display_username = display_username;
254
255 if let Some(username) = signup_req.username.as_deref() {
256 match validate_username(username) {
257 Ok(()) => {}
258 Err(UsernameValidationError::TooShort) => {
259 return username_error_response(
260 400,
261 "USERNAME_TOO_SHORT",
262 MESSAGE_USERNAME_TOO_SHORT,
263 );
264 }
265 Err(UsernameValidationError::TooLong) => {
266 return username_error_response(
267 400,
268 "USERNAME_TOO_LONG",
269 MESSAGE_USERNAME_TOO_LONG,
270 );
271 }
272 Err(UsernameValidationError::Invalid) => {
273 return username_error_response(
274 400,
275 "INVALID_USERNAME",
276 MESSAGE_INVALID_USERNAME,
277 );
278 }
279 }
280
281 if ctx.database.get_user_by_username(username).await?.is_some() {
285 return username_error_response(
286 400,
287 "USERNAME_IS_ALREADY_TAKEN",
288 MESSAGE_USERNAME_IS_ALREADY_TAKEN,
289 );
290 }
291 }
292
293 let meta = RequestMeta::from_request(req);
294 let (response, session_token) = sign_up_core(&signup_req, &self.config, &meta, ctx).await?;
295
296 if let Some(token) = session_token {
297 let cookie_header = create_session_cookie(&token, &ctx.config);
298 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
299 } else {
300 Ok(AuthResponse::json(200, &response)?)
301 }
302 }
303
304 async fn handle_sign_in(
305 &self,
306 req: &AuthRequest,
307 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
308 ) -> AuthResult<AuthResponse> {
309 if let Ok(raw_body) = req.body_as_json::<serde_json::Value>()
310 && let Some(email) = raw_body.get("email").and_then(|value| value.as_str())
311 && !email.validate_email()
312 {
313 return Err(AuthError::bad_request("Invalid email"));
314 }
315
316 let signin_req: SignInRequest = match better_auth_core::validate_request_body(req) {
317 Ok(v) => v,
318 Err(resp) => return Ok(resp),
319 };
320
321 let meta = RequestMeta::from_request(req);
322 match sign_in_core(
323 req,
324 &signin_req,
325 &self.config,
326 self.email_verification.as_deref(),
327 &meta,
328 ctx,
329 )
330 .await?
331 {
332 SignInCoreResult::Success {
333 response,
334 token,
335 set_cookie_headers,
336 } => {
337 let mut auth_response = AuthResponse::json(200, &response)?.with_appended_header(
338 "Set-Cookie",
339 create_session_cookie_for_remember_me(
340 &token,
341 signin_req.remember_me,
342 &ctx.config,
343 ),
344 );
345 for cookie in set_cookie_headers {
346 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
347 }
348 Ok(auth_response)
349 }
350 SignInCoreResult::TwoFactorRedirect {
351 response,
352 set_cookie_headers,
353 } => {
354 let mut auth_response = AuthResponse::json(200, &response)?;
355 for cookie in set_cookie_headers {
356 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
357 }
358 Ok(auth_response)
359 }
360 }
361 }
362
363 async fn handle_sign_in_username(
364 &self,
365 req: &AuthRequest,
366 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
367 ) -> AuthResult<AuthResponse> {
368 let signin_req: SignInUsernameRequest = match better_auth_core::validate_request_body(req) {
369 Ok(v) => v,
370 Err(resp) => return Ok(resp),
371 };
372
373 if signin_req.username.is_empty() || signin_req.password.is_empty() {
374 return username_error_response(
375 401,
376 "INVALID_USERNAME_OR_PASSWORD",
377 MESSAGE_INVALID_USERNAME_OR_PASSWORD,
378 );
379 }
380
381 let username = normalize_username(&signin_req.username);
382
383 match validate_username(&username) {
384 Ok(()) => {}
385 Err(UsernameValidationError::TooShort) => {
386 return username_error_response(
387 422,
388 "USERNAME_TOO_SHORT",
389 MESSAGE_USERNAME_TOO_SHORT,
390 );
391 }
392 Err(UsernameValidationError::TooLong) => {
393 return username_error_response(
394 422,
395 "USERNAME_TOO_LONG",
396 MESSAGE_USERNAME_TOO_LONG,
397 );
398 }
399 Err(UsernameValidationError::Invalid) => {
400 return username_error_response(422, "INVALID_USERNAME", MESSAGE_INVALID_USERNAME);
401 }
402 }
403
404 let meta = RequestMeta::from_request(req);
405 match sign_in_username_core(
406 req,
407 &signin_req,
408 &username,
409 &self.config,
410 self.email_verification.as_deref(),
411 &meta,
412 ctx,
413 )
414 .await
415 {
416 Ok(SignInCoreResult::Success {
417 response,
418 token,
419 set_cookie_headers,
420 }) => {
421 let username_response = SignInUsernameResponse {
422 redirect: response.redirect,
423 token: response.token,
424 url: response.url,
425 user: response.user,
426 };
427 let mut auth_response = AuthResponse::json(200, &username_response)?
428 .with_appended_header(
429 "Set-Cookie",
430 create_session_cookie_for_remember_me(
431 &token,
432 signin_req.remember_me,
433 &ctx.config,
434 ),
435 );
436 for cookie in set_cookie_headers {
437 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
438 }
439 Ok(auth_response)
440 }
441 Ok(SignInCoreResult::TwoFactorRedirect {
442 response,
443 set_cookie_headers,
444 }) => {
445 let mut auth_response = AuthResponse::json(200, &response)?;
446 for cookie in set_cookie_headers {
447 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
448 }
449 Ok(auth_response)
450 }
451 Err(SignInUsernameFailure::InvalidUsernameOrPassword) => username_error_response(
452 401,
453 "INVALID_USERNAME_OR_PASSWORD",
454 MESSAGE_INVALID_USERNAME_OR_PASSWORD,
455 ),
456 Err(SignInUsernameFailure::EmailNotVerified) => {
457 username_error_response(403, "EMAIL_NOT_VERIFIED", MESSAGE_EMAIL_NOT_VERIFIED)
458 }
459 Err(SignInUsernameFailure::Auth(error)) => Err(error),
460 }
461 }
462
463 async fn handle_is_username_available(
464 &self,
465 req: &AuthRequest,
466 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
467 ) -> AuthResult<AuthResponse> {
468 let body: IsUsernameAvailableRequest = match better_auth_core::validate_request_body(req) {
469 Ok(v) => v,
470 Err(resp) => return Ok(resp),
471 };
472
473 match validate_username(&body.username) {
474 Ok(()) => {}
475 Err(UsernameValidationError::TooShort) => {
476 return username_error_response(
477 422,
478 "USERNAME_TOO_SHORT",
479 MESSAGE_USERNAME_TOO_SHORT,
480 );
481 }
482 Err(UsernameValidationError::TooLong) => {
483 return username_error_response(
484 422,
485 "USERNAME_TOO_LONG",
486 MESSAGE_USERNAME_TOO_LONG,
487 );
488 }
489 Err(UsernameValidationError::Invalid) => {
490 return username_error_response(422, "INVALID_USERNAME", MESSAGE_INVALID_USERNAME);
491 }
492 }
493
494 let normalized = normalize_username(&body.username);
495 let user = ctx.database.get_user_by_username(&normalized).await?;
496 let available = user.is_none();
497
498 Ok(AuthResponse::json(
499 200,
500 &IsUsernameAvailableResponse { available },
501 )?)
502 }
503}
504
505pub(crate) async fn sign_up_core(
514 body: &SignUpRequest,
515 config: &EmailPasswordConfig,
516 meta: &RequestMeta,
517 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
518) -> AuthResult<(SignUpResponse<UserView>, Option<String>)> {
519 if !config.enable_signup {
520 return Err(AuthError::forbidden("User registration is not enabled"));
521 }
522
523 password_utils::validate_password(
524 &body.password,
525 config.password_min_length,
526 config.password_max_length,
527 ctx,
528 )?;
529
530 if ctx.database.get_user_by_email(&body.email).await?.is_some() {
532 return Err(AuthError::UnprocessableEntity(
534 "User already exists. Use another email.".to_string(),
535 ));
536 }
537
538 let password_hash =
540 password_utils::hash_password(config.password_hasher.as_ref(), &body.password).await?;
541
542 let mut create_user = CreateUser::new()
543 .with_email(&body.email)
544 .with_name(&body.name);
545 apply_default_role(ctx, &mut create_user);
546 if let Some(ref username) = body.username {
547 create_user = create_user.with_username(normalize_username(username));
548 }
549 if let Some(ref display_username) = body.display_username {
550 create_user.display_username = Some(display_username.clone());
551 } else if let Some(ref username) = body.username {
552 create_user.display_username = Some(username.clone());
553 }
554 let auto_sign_in = config.auto_sign_in;
555 let expires_in = ctx.config.session.expires_in;
556 let ip_address = meta.ip_address.clone();
557 let user_agent = meta.user_agent.clone();
558 let database = ctx.database.clone();
559 let transaction_database = database.clone();
560
561 better_auth_core::store::transaction(database.as_ref(), move |tx| {
562 let _database = transaction_database.clone();
563 Box::pin(async move {
564 let user = match tx.create_user(create_user).await {
565 Ok(user) => user,
566 Err(AuthError::Database(_)) => {
567 return Err(AuthError::UnprocessableEntity(
568 "Failed to create user".to_string(),
569 ));
570 }
571 Err(error) => return Err(error),
572 };
573
574 let _ = tx
575 .create_account(CreateAccount {
576 user_id: user.id().to_string(),
577 account_id: user.id().to_string(),
578 provider_id: "credential".to_string(),
579 access_token: None,
580 refresh_token: None,
581 id_token: None,
582 access_token_expires_at: None,
583 refresh_token_expires_at: None,
584 scope: None,
585 password: Some(password_hash.clone()),
586 })
587 .await?;
588
589 if auto_sign_in {
590 let session = tx
591 .create_session(CreateSession {
592 user_id: user.id().to_string(),
593 expires_at: chrono::Utc::now() + expires_in,
594 ip_address,
595 user_agent,
596 impersonated_by: None,
597 active_organization_id: None,
598 })
599 .await?;
600 let token = session.token().to_string();
601
602 Ok((
603 SignUpResponse {
604 token: Some(token.clone()),
605 user: UserView::from(&user),
606 },
607 Some(token),
608 ))
609 } else {
610 Ok((
611 SignUpResponse {
612 token: None,
613 user: UserView::from(&user),
614 },
615 None,
616 ))
617 }
618 })
619 })
620 .await
621}
622
623async fn load_credential_password_hash(
624 user: &impl AuthUser,
625 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
626) -> AuthResult<String> {
627 ctx.database
628 .get_user_accounts(&user.id())
629 .await?
630 .into_iter()
631 .find(|account| account.provider_id() == "credential" && account.password().is_some())
632 .and_then(|account| account.password().map(str::to_string))
633 .ok_or(AuthError::InvalidCredentials)
634}
635
636async fn verify_user_password(
637 user: &impl AuthUser,
638 password: &str,
639 config: &EmailPasswordConfig,
640 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
641) -> AuthResult<()> {
642 let stored_hash = load_credential_password_hash(user, ctx).await?;
643 password_utils::verify_password(config.password_hasher.as_ref(), password, &stored_hash).await
644}
645
646async fn finalize_sign_in_with_user_core(
648 req: &AuthRequest,
649 user: impl AuthUser,
650 remember_me: Option<bool>,
651 email_verification: Option<&EmailVerificationPlugin>,
652 callback_url: Option<&str>,
653 meta: &RequestMeta,
654 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
655) -> AuthResult<SignInCoreResult<UserView>> {
656 let mut set_cookie_headers = Vec::new();
657 if two_factor::is_enabled(ctx) && user.two_factor_enabled() {
658 let trusted_device = two_factor::inspect_trusted_device(req, &user, ctx).await?;
659 if trusted_device.trusted {
660 set_cookie_headers.extend(trusted_device.set_cookie_headers);
661 } else {
662 let redirect = two_factor::begin_sign_in_challenge(&user, remember_me, ctx).await?;
663 let mut redirect_headers = trusted_device.set_cookie_headers;
664 redirect_headers.extend(redirect.set_cookie_headers);
665 return Ok(SignInCoreResult::TwoFactorRedirect {
666 response: redirect.response,
667 set_cookie_headers: redirect_headers,
668 });
669 }
670 }
671
672 if let Some(ev) = email_verification
674 && let Err(e) = ev
675 .send_verification_on_sign_in(&user, callback_url, ctx)
676 .await
677 {
678 tracing::warn!(
679 error = %e,
680 "Failed to send verification email on sign-in"
681 );
682 }
683
684 let issued = issue_user_session(
685 ctx,
686 &user.id(),
687 meta.ip_address.clone(),
688 meta.user_agent.clone(),
689 )
690 .await
691 .map_err(SessionIssueError::into_auth_error)?;
692 let session = issued.session;
693 let token = session.token().to_string();
694
695 let response = SignInResponse {
696 redirect: false,
697 token: token.clone(),
698 url: None,
699 user: UserView::from(&issued.user),
700 };
701 Ok(SignInCoreResult::Success {
702 response,
703 token,
704 set_cookie_headers,
705 })
706}
707
708pub(crate) async fn sign_in_core(
710 req: &AuthRequest,
711 body: &SignInRequest,
712 config: &EmailPasswordConfig,
713 email_verification: Option<&EmailVerificationPlugin>,
714 meta: &RequestMeta,
715 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
716) -> AuthResult<SignInCoreResult<UserView>> {
717 let user = ctx
718 .database
719 .get_user_by_email(&body.email)
720 .await?
721 .ok_or(AuthError::InvalidCredentials)?;
722
723 verify_user_password(&user, &body.password, config, ctx).await?;
724
725 finalize_sign_in_with_user_core(
726 req,
727 user,
728 body.remember_me,
729 email_verification,
730 body.callback_url.as_deref(),
731 meta,
732 ctx,
733 )
734 .await
735}
736
737pub(crate) async fn sign_in_username_core(
739 req: &AuthRequest,
740 body: &SignInUsernameRequest,
741 normalized_username: &str,
742 config: &EmailPasswordConfig,
743 email_verification: Option<&EmailVerificationPlugin>,
744 meta: &RequestMeta,
745 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
746) -> Result<SignInCoreResult<UserView>, SignInUsernameFailure> {
747 let Some(user) = ctx
748 .database
749 .get_user_by_username(normalized_username)
750 .await
751 .map_err(SignInUsernameFailure::Auth)?
752 else {
753 let _ = password_utils::hash_password(config.password_hasher.as_ref(), &body.password)
754 .await
755 .map_err(SignInUsernameFailure::Auth)?;
756 return Err(SignInUsernameFailure::InvalidUsernameOrPassword);
757 };
758
759 verify_user_password(&user, &body.password, config, ctx)
760 .await
761 .map_err(|error| match error {
762 AuthError::InvalidCredentials => SignInUsernameFailure::InvalidUsernameOrPassword,
763 other => SignInUsernameFailure::Auth(other),
764 })?;
765
766 if let Some(ev) = email_verification
767 && ev.is_verification_required()
768 && !user.email_verified()
769 {
770 if let Err(error) = ev
771 .send_verification_on_sign_in(&user, body.callback_url.as_deref(), ctx)
772 .await
773 {
774 tracing::warn!(
775 error = %error,
776 "Failed to send verification email on username sign-in"
777 );
778 }
779 return Err(SignInUsernameFailure::EmailNotVerified);
780 }
781
782 finalize_sign_in_with_user_core(
783 req,
784 user,
785 body.remember_me,
786 email_verification,
787 body.callback_url.as_deref(),
788 meta,
789 ctx,
790 )
791 .await
792 .map_err(SignInUsernameFailure::Auth)
793}
794
795pub(crate) enum SignInUsernameFailure {
796 InvalidUsernameOrPassword,
797 EmailNotVerified,
798 Auth(AuthError),
799}
800
801impl Default for EmailPasswordConfig {
802 fn default() -> Self {
803 Self {
804 enable_signup: true,
805 require_email_verification: false,
806 password_min_length: 8,
807 password_max_length: 128,
808 auto_sign_in: true,
809 password_hasher: None,
810 }
811 }
812}
813
814#[async_trait]
815impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for EmailPasswordPlugin {
816 fn name(&self) -> &'static str {
817 "email-password"
818 }
819
820 fn routes(&self) -> Vec<AuthRoute> {
821 let mut routes = vec![
822 AuthRoute::post("/sign-in/email", "sign_in_email"),
823 AuthRoute::post("/sign-in/username", "sign_in_username"),
824 AuthRoute::post("/is-username-available", "is_username_available"),
825 ];
826
827 if self.config.enable_signup {
828 routes.push(AuthRoute::post("/sign-up/email", "sign_up_email"));
829 }
830
831 routes
832 }
833
834 async fn on_request(
835 &self,
836 req: &AuthRequest,
837 ctx: &AuthContext<S>,
838 ) -> AuthResult<Option<AuthResponse>> {
839 match (req.method(), req.path()) {
840 (HttpMethod::Post, "/sign-up/email") if self.config.enable_signup => {
841 Ok(Some(self.handle_sign_up(req, ctx).await?))
842 }
843 (HttpMethod::Post, "/sign-in/email") => Ok(Some(self.handle_sign_in(req, ctx).await?)),
844 (HttpMethod::Post, "/sign-in/username") => {
845 Ok(Some(self.handle_sign_in_username(req, ctx).await?))
846 }
847 (HttpMethod::Post, "/is-username-available") => {
848 Ok(Some(self.handle_is_username_available(req, ctx).await?))
849 }
850 _ => Ok(None),
851 }
852 }
853
854 async fn on_user_created(&self, user: &S::User, _ctx: &AuthContext<S>) -> AuthResult<()> {
855 if self.config.require_email_verification
856 && !user.email_verified()
857 && let Some(email) = user.email()
858 {
859 println!("Email verification required for user: {}", email);
860 }
861 Ok(())
862 }
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use better_auth_core::AuthContext;
869 use better_auth_core::config::AuthConfig;
870 use std::collections::HashMap;
871 use std::sync::Arc;
872 use std::sync::atomic::{AtomicUsize, Ordering};
873
874 type TestSchema =
875 better_auth_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
876
877 async fn create_test_context() -> AuthContext<TestSchema> {
878 let config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
879 let config = Arc::new(config);
880 let database = crate::plugins::test_helpers::create_test_database().await;
881 AuthContext::new(config, database)
882 }
883
884 fn create_signup_request(email: &str, password: &str) -> AuthRequest {
885 let body = serde_json::json!({
886 "name": "Test User",
887 "email": email,
888 "password": password,
889 });
890 AuthRequest::from_parts(
891 HttpMethod::Post,
892 "/sign-up/email".to_string(),
893 HashMap::new(),
894 Some(body.to_string().into_bytes()),
895 HashMap::new(),
896 )
897 }
898
899 #[tokio::test]
901 async fn test_auto_sign_in_false_returns_no_session() {
902 let plugin = EmailPasswordPlugin::new().auto_sign_in(false);
903 let ctx = create_test_context().await;
904
905 let req = create_signup_request("auto@example.com", "Password123!");
906 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
907 assert_eq!(response.status, 200);
908
909 let has_cookie = response
911 .headers
912 .iter()
913 .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
914 assert!(!has_cookie, "auto_sign_in=false should not set a cookie");
915
916 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
918 assert!(
919 body["token"].is_null(),
920 "auto_sign_in=false should return null token"
921 );
922 assert!(body["user"]["id"].is_string());
924 }
925
926 #[tokio::test]
928 async fn test_auto_sign_in_true_returns_session() {
929 let plugin = EmailPasswordPlugin::new(); let ctx = create_test_context().await;
931
932 let req = create_signup_request("autotrue@example.com", "Password123!");
933 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
934 assert_eq!(response.status, 200);
935
936 let has_cookie = response
938 .headers
939 .iter()
940 .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
941 assert!(has_cookie, "auto_sign_in=true should set a cookie");
942
943 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
945 assert!(
946 body["token"].is_string(),
947 "auto_sign_in=true should return a session token"
948 );
949 }
950
951 #[tokio::test]
953 async fn test_password_max_length_rejection() {
954 let plugin = EmailPasswordPlugin::new().password_max_length(128);
955 let ctx = create_test_context().await;
956
957 let long_password = format!("A1!{}", "a".repeat(126)); let req = create_signup_request("long@example.com", &long_password);
960 let err = plugin.handle_sign_up(&req, &ctx).await.unwrap_err();
961 assert_eq!(err.status_code(), 400);
962
963 let ok_password = format!("A1!{}", "a".repeat(125)); let req = create_signup_request("ok@example.com", &ok_password);
966 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
967 assert_eq!(response.status, 200);
968 }
969
970 #[tokio::test]
972 async fn test_custom_password_hasher() {
973 struct TestHasher;
975
976 #[async_trait]
977 impl PasswordHasher for TestHasher {
978 async fn hash(&self, password: &str) -> AuthResult<String> {
979 Ok(format!("hashed:{}", password))
980 }
981 async fn verify(&self, hash: &str, password: &str) -> AuthResult<bool> {
982 Ok(hash == format!("hashed:{}", password))
983 }
984 }
985
986 let hasher: Arc<dyn PasswordHasher> = Arc::new(TestHasher);
987 let plugin = EmailPasswordPlugin::new().password_hasher(hasher);
988 let ctx = create_test_context().await;
989
990 let req = create_signup_request("hasher@example.com", "Password123!");
992 let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
993 assert_eq!(response.status, 200);
994
995 let user = ctx
997 .database
998 .get_user_by_email("hasher@example.com")
999 .await
1000 .unwrap()
1001 .unwrap();
1002 let stored_hash = ctx
1003 .database
1004 .get_user_accounts(&user.id())
1005 .await
1006 .unwrap()
1007 .into_iter()
1008 .find(|account| account.provider_id() == "credential")
1009 .and_then(|account| account.password().map(str::to_string))
1010 .expect("credential account should store hashed password");
1011 assert_eq!(stored_hash, "hashed:Password123!");
1012
1013 let signin_body = serde_json::json!({
1015 "email": "hasher@example.com",
1016 "password": "Password123!",
1017 });
1018 let signin_req = AuthRequest::from_parts(
1019 HttpMethod::Post,
1020 "/sign-in/email".to_string(),
1021 HashMap::new(),
1022 Some(signin_body.to_string().into_bytes()),
1023 HashMap::new(),
1024 );
1025 let response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
1026 assert_eq!(response.status, 200);
1027
1028 let bad_body = serde_json::json!({
1030 "email": "hasher@example.com",
1031 "password": "WrongPassword!",
1032 });
1033 let bad_req = AuthRequest::from_parts(
1034 HttpMethod::Post,
1035 "/sign-in/email".to_string(),
1036 HashMap::new(),
1037 Some(bad_body.to_string().into_bytes()),
1038 HashMap::new(),
1039 );
1040 let err = plugin.handle_sign_in(&bad_req, &ctx).await.unwrap_err();
1041 assert_eq!(err.to_string(), AuthError::InvalidCredentials.to_string());
1042 }
1043
1044 #[tokio::test]
1046 async fn test_sign_in_username_verifies_password_once() {
1047 struct CountingHasher {
1048 verify_calls: Arc<AtomicUsize>,
1049 }
1050
1051 #[async_trait]
1052 impl PasswordHasher for CountingHasher {
1053 async fn hash(&self, password: &str) -> AuthResult<String> {
1054 Ok(format!("hashed:{password}"))
1055 }
1056
1057 async fn verify(&self, hash: &str, password: &str) -> AuthResult<bool> {
1058 self.verify_calls.fetch_add(1, Ordering::SeqCst);
1059 Ok(hash == format!("hashed:{password}"))
1060 }
1061 }
1062
1063 let verify_calls = Arc::new(AtomicUsize::new(0));
1064 let hasher: Arc<dyn PasswordHasher> = Arc::new(CountingHasher {
1065 verify_calls: verify_calls.clone(),
1066 });
1067 let plugin = EmailPasswordPlugin::new().password_hasher(hasher);
1068 let ctx = create_test_context().await;
1069
1070 let signup_body = serde_json::json!({
1071 "email": "username-counter@example.com",
1072 "password": "Password123!",
1073 "name": "Counter User",
1074 "username": "Counter_User",
1075 });
1076 let signup_req = AuthRequest::from_parts(
1077 HttpMethod::Post,
1078 "/sign-up/email".to_string(),
1079 HashMap::new(),
1080 Some(signup_body.to_string().into_bytes()),
1081 HashMap::new(),
1082 );
1083 let signup_response = plugin.handle_sign_up(&signup_req, &ctx).await.unwrap();
1084 assert_eq!(signup_response.status, 200);
1085
1086 verify_calls.store(0, Ordering::SeqCst);
1087
1088 let signin_body = serde_json::json!({
1089 "username": "COUNTER_USER",
1090 "password": "Password123!",
1091 });
1092 let signin_req = AuthRequest::from_parts(
1093 HttpMethod::Post,
1094 "/sign-in/username".to_string(),
1095 HashMap::new(),
1096 Some(signin_body.to_string().into_bytes()),
1097 HashMap::new(),
1098 );
1099 let signin_response = plugin
1100 .handle_sign_in_username(&signin_req, &ctx)
1101 .await
1102 .unwrap();
1103 assert_eq!(signin_response.status, 200);
1104 assert_eq!(verify_calls.load(Ordering::SeqCst), 1);
1105 }
1106
1107 #[tokio::test]
1110 async fn test_is_username_available_route_registered() {
1111 let plugin = EmailPasswordPlugin::new();
1112 let routes =
1113 <EmailPasswordPlugin as better_auth_core::AuthPlugin<TestSchema>>::routes(&plugin);
1114 assert!(
1115 routes.iter().any(|r| r.path == "/is-username-available"),
1116 "route /is-username-available should be registered"
1117 );
1118 }
1119
1120 #[tokio::test]
1124 async fn test_is_username_available_fresh() {
1125 let plugin = EmailPasswordPlugin::new();
1126 let ctx = create_test_context().await;
1127
1128 let body = serde_json::json!({ "username": "fresh_user" });
1129 let req = AuthRequest::from_parts(
1130 HttpMethod::Post,
1131 "/is-username-available".to_string(),
1132 HashMap::new(),
1133 Some(body.to_string().into_bytes()),
1134 HashMap::new(),
1135 );
1136 let response = plugin
1137 .handle_is_username_available(&req, &ctx)
1138 .await
1139 .unwrap();
1140 assert_eq!(response.status, 200);
1141 let json: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1142 assert_eq!(json["available"], true);
1143 }
1144
1145 #[tokio::test]
1149 async fn test_is_username_available_taken() {
1150 let plugin = EmailPasswordPlugin::new();
1151 let ctx = create_test_context().await;
1152
1153 let signup_body = serde_json::json!({
1155 "name": "Taken User",
1156 "email": "taken@example.com",
1157 "password": "Password123!",
1158 "username": "taken_user",
1159 });
1160 let signup_req = AuthRequest::from_parts(
1161 HttpMethod::Post,
1162 "/sign-up/email".to_string(),
1163 HashMap::new(),
1164 Some(signup_body.to_string().into_bytes()),
1165 HashMap::new(),
1166 );
1167 let resp = plugin.handle_sign_up(&signup_req, &ctx).await.unwrap();
1168 assert_eq!(resp.status, 200);
1169
1170 let body = serde_json::json!({ "username": "taken_user" });
1171 let req = AuthRequest::from_parts(
1172 HttpMethod::Post,
1173 "/is-username-available".to_string(),
1174 HashMap::new(),
1175 Some(body.to_string().into_bytes()),
1176 HashMap::new(),
1177 );
1178 let response = plugin
1179 .handle_is_username_available(&req, &ctx)
1180 .await
1181 .unwrap();
1182 assert_eq!(response.status, 200);
1183 let json: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1184 assert_eq!(json["available"], false);
1185 }
1186
1187 #[tokio::test]
1191 async fn test_is_username_available_too_short() {
1192 let plugin = EmailPasswordPlugin::new();
1193 let ctx = create_test_context().await;
1194
1195 let body = serde_json::json!({ "username": "ab" });
1196 let req = AuthRequest::from_parts(
1197 HttpMethod::Post,
1198 "/is-username-available".to_string(),
1199 HashMap::new(),
1200 Some(body.to_string().into_bytes()),
1201 HashMap::new(),
1202 );
1203 let response = plugin
1204 .handle_is_username_available(&req, &ctx)
1205 .await
1206 .unwrap();
1207 assert_eq!(response.status, 422);
1208 let json: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1209 assert_eq!(json["code"], "USERNAME_TOO_SHORT");
1210 }
1211
1212 #[tokio::test]
1216 async fn test_is_username_available_invalid_chars() {
1217 let plugin = EmailPasswordPlugin::new();
1218 let ctx = create_test_context().await;
1219
1220 let body = serde_json::json!({ "username": "bad user!" });
1221 let req = AuthRequest::from_parts(
1222 HttpMethod::Post,
1223 "/is-username-available".to_string(),
1224 HashMap::new(),
1225 Some(body.to_string().into_bytes()),
1226 HashMap::new(),
1227 );
1228 let response = plugin
1229 .handle_is_username_available(&req, &ctx)
1230 .await
1231 .unwrap();
1232 assert_eq!(response.status, 422);
1233 let json: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1234 assert_eq!(json["code"], "INVALID_USERNAME");
1235 }
1236}