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