1use async_trait::async_trait;
2use chrono::{Duration, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5use validator::Validate;
6
7use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
8use better_auth_core::{AuthError, AuthResult};
9use better_auth_core::{AuthRequest, AuthResponse, CreateVerification, HttpMethod, UpdateUser};
10use better_auth_core::{AuthSession, AuthUser, AuthVerification, DatabaseAdapter};
11
12pub struct PasswordManagementPlugin {
14 config: PasswordManagementConfig,
15}
16
17#[derive(Debug, Clone)]
18pub struct PasswordManagementConfig {
19 pub reset_token_expiry_hours: i64,
20 pub require_current_password: bool,
21 pub send_email_notifications: bool,
22}
23
24#[derive(Debug, Deserialize, Validate)]
26struct ForgetPasswordRequest {
27 #[validate(email(message = "Invalid email address"))]
28 email: String,
29 #[serde(rename = "redirectTo")]
30 redirect_to: Option<String>,
31}
32
33#[derive(Debug, Deserialize, Validate)]
34struct ResetPasswordRequest {
35 #[serde(rename = "newPassword")]
36 #[validate(length(min = 1, message = "New password is required"))]
37 new_password: String,
38 token: Option<String>,
39}
40
41#[derive(Debug, Deserialize, Validate)]
42struct SetPasswordRequest {
43 #[serde(rename = "newPassword")]
44 #[validate(length(min = 1, message = "New password is required"))]
45 new_password: String,
46}
47
48#[derive(Debug, Deserialize, Validate)]
49struct ChangePasswordRequest {
50 #[serde(rename = "newPassword")]
51 #[validate(length(min = 1, message = "New password is required"))]
52 new_password: String,
53 #[serde(rename = "currentPassword")]
54 #[validate(length(min = 1, message = "Current password is required"))]
55 current_password: String,
56 #[serde(rename = "revokeOtherSessions")]
57 revoke_other_sessions: Option<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
62struct StatusResponse {
63 status: bool,
64}
65
66#[derive(Debug, Serialize)]
67struct ChangePasswordResponse<U: Serialize> {
68 token: Option<String>,
69 user: U,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
73struct ResetPasswordTokenResponse {
74 token: String,
75}
76
77impl PasswordManagementPlugin {
78 pub fn new() -> Self {
79 Self {
80 config: PasswordManagementConfig::default(),
81 }
82 }
83
84 pub fn with_config(config: PasswordManagementConfig) -> Self {
85 Self { config }
86 }
87
88 pub fn reset_token_expiry_hours(mut self, hours: i64) -> Self {
89 self.config.reset_token_expiry_hours = hours;
90 self
91 }
92
93 pub fn require_current_password(mut self, require: bool) -> Self {
94 self.config.require_current_password = require;
95 self
96 }
97
98 pub fn send_email_notifications(mut self, send: bool) -> Self {
99 self.config.send_email_notifications = send;
100 self
101 }
102}
103
104impl Default for PasswordManagementPlugin {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl Default for PasswordManagementConfig {
111 fn default() -> Self {
112 Self {
113 reset_token_expiry_hours: 24, require_current_password: true,
115 send_email_notifications: true,
116 }
117 }
118}
119
120#[async_trait]
121impl<DB: DatabaseAdapter> AuthPlugin<DB> for PasswordManagementPlugin {
122 fn name(&self) -> &'static str {
123 "password-management"
124 }
125
126 fn routes(&self) -> Vec<AuthRoute> {
127 vec![
128 AuthRoute::post("/forget-password", "forget_password"),
129 AuthRoute::post("/reset-password", "reset_password"),
130 AuthRoute::get("/reset-password/{token}", "reset_password_token"),
131 AuthRoute::post("/change-password", "change_password"),
132 AuthRoute::post("/set-password", "set_password"),
133 ]
134 }
135
136 async fn on_request(
137 &self,
138 req: &AuthRequest,
139 ctx: &AuthContext<DB>,
140 ) -> AuthResult<Option<AuthResponse>> {
141 match (req.method(), req.path()) {
142 (HttpMethod::Post, "/forget-password") => {
143 Ok(Some(self.handle_forget_password(req, ctx).await?))
144 }
145 (HttpMethod::Post, "/reset-password") => {
146 Ok(Some(self.handle_reset_password(req, ctx).await?))
147 }
148 (HttpMethod::Post, "/change-password") => {
149 Ok(Some(self.handle_change_password(req, ctx).await?))
150 }
151 (HttpMethod::Post, "/set-password") => {
152 Ok(Some(self.handle_set_password(req, ctx).await?))
153 }
154 (HttpMethod::Get, path) if path.starts_with("/reset-password/") => {
155 let token = &path[16..]; Ok(Some(
157 self.handle_reset_password_token(token, req, ctx).await?,
158 ))
159 }
160 _ => Ok(None),
161 }
162 }
163}
164
165impl PasswordManagementPlugin {
167 async fn handle_forget_password<DB: DatabaseAdapter>(
168 &self,
169 req: &AuthRequest,
170 ctx: &AuthContext<DB>,
171 ) -> AuthResult<AuthResponse> {
172 let forget_req: ForgetPasswordRequest = match better_auth_core::validate_request_body(req) {
173 Ok(v) => v,
174 Err(resp) => return Ok(resp),
175 };
176
177 let user = match ctx.database.get_user_by_email(&forget_req.email).await? {
179 Some(user) => user,
180 None => {
181 let response = StatusResponse { status: true };
183 return Ok(AuthResponse::json(200, &response)?);
184 }
185 };
186
187 let reset_token = format!("reset_{}", Uuid::new_v4());
189 let expires_at = Utc::now() + Duration::hours(self.config.reset_token_expiry_hours);
190
191 let create_verification = CreateVerification {
193 identifier: user.email().unwrap_or_default().to_string(),
194 value: reset_token.clone(),
195 expires_at,
196 };
197
198 ctx.database
199 .create_verification(create_verification)
200 .await?;
201
202 if self.config.send_email_notifications {
204 let reset_url = if let Some(redirect_to) = &forget_req.redirect_to {
205 format!("{}?token={}", redirect_to, reset_token)
206 } else {
207 format!(
208 "{}/reset-password?token={}",
209 ctx.config.base_url, reset_token
210 )
211 };
212
213 if let Ok(provider) = ctx.email_provider() {
214 let subject = "Reset your password";
215 let html = format!(
216 "<p>Click the link below to reset your password:</p>\
217 <p><a href=\"{url}\">Reset Password</a></p>",
218 url = reset_url
219 );
220 let text = format!("Reset your password: {}", reset_url);
221
222 if let Err(e) = provider
223 .send(&forget_req.email, subject, &html, &text)
224 .await
225 {
226 eprintln!(
227 "[password-management] Failed to send reset email to {}: {}",
228 forget_req.email, e
229 );
230 }
231 } else {
232 eprintln!(
233 "[password-management] No email provider configured, skipping password reset email for {}",
234 forget_req.email
235 );
236 }
237 }
238
239 let response = StatusResponse { status: true };
240 Ok(AuthResponse::json(200, &response)?)
241 }
242
243 async fn handle_reset_password<DB: DatabaseAdapter>(
244 &self,
245 req: &AuthRequest,
246 ctx: &AuthContext<DB>,
247 ) -> AuthResult<AuthResponse> {
248 let reset_req: ResetPasswordRequest = match better_auth_core::validate_request_body(req) {
249 Ok(v) => v,
250 Err(resp) => return Ok(resp),
251 };
252
253 self.validate_password(&reset_req.new_password, ctx)?;
255
256 let token = reset_req.token.as_deref().unwrap_or("");
258 if token.is_empty() {
259 return Err(AuthError::bad_request("Reset token is required"));
260 }
261
262 let (user, verification) = self
263 .find_user_by_reset_token(token, ctx)
264 .await?
265 .ok_or_else(|| AuthError::bad_request("Invalid or expired reset token"))?;
266
267 let password_hash = self.hash_password(&reset_req.new_password)?;
269
270 let mut metadata = user.metadata().clone();
272 metadata.insert(
273 "password_hash".to_string(),
274 serde_json::Value::String(password_hash),
275 );
276
277 let update_user = UpdateUser {
278 email: None,
279 name: None,
280 image: None,
281 email_verified: None,
282 username: None,
283 display_username: None,
284 role: None,
285 banned: None,
286 ban_reason: None,
287 ban_expires: None,
288 two_factor_enabled: None,
289 metadata: Some(metadata),
290 };
291
292 ctx.database.update_user(user.id(), update_user).await?;
293
294 ctx.database.delete_verification(verification.id()).await?;
296
297 ctx.database.delete_user_sessions(user.id()).await?;
299
300 let response = StatusResponse { status: true };
301 Ok(AuthResponse::json(200, &response)?)
302 }
303
304 async fn handle_change_password<DB: DatabaseAdapter>(
305 &self,
306 req: &AuthRequest,
307 ctx: &AuthContext<DB>,
308 ) -> AuthResult<AuthResponse> {
309 let change_req: ChangePasswordRequest = match better_auth_core::validate_request_body(req) {
310 Ok(v) => v,
311 Err(resp) => return Ok(resp),
312 };
313
314 let user = self
316 .get_current_user(req, ctx)
317 .await?
318 .ok_or(AuthError::Unauthenticated)?;
319
320 if self.config.require_current_password {
322 let stored_hash = user
323 .metadata()
324 .get("password_hash")
325 .and_then(|v| v.as_str())
326 .ok_or_else(|| AuthError::bad_request("No password set for this user"))?;
327
328 self.verify_password(&change_req.current_password, stored_hash)
329 .map_err(|_| AuthError::InvalidCredentials)?;
330 }
331
332 self.validate_password(&change_req.new_password, ctx)?;
334
335 let password_hash = self.hash_password(&change_req.new_password)?;
337
338 let mut metadata = user.metadata().clone();
340 metadata.insert(
341 "password_hash".to_string(),
342 serde_json::Value::String(password_hash),
343 );
344
345 let update_user = UpdateUser {
346 email: None,
347 name: None,
348 image: None,
349 email_verified: None,
350 username: None,
351 display_username: None,
352 role: None,
353 banned: None,
354 ban_reason: None,
355 ban_expires: None,
356 two_factor_enabled: None,
357 metadata: Some(metadata),
358 };
359
360 let updated_user = ctx.database.update_user(user.id(), update_user).await?;
361
362 let new_token = if change_req.revoke_other_sessions.as_deref() == Some("true") {
364 ctx.database.delete_user_sessions(user.id()).await?;
366
367 let session_manager =
369 better_auth_core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
370 let session = session_manager
371 .create_session(&updated_user, None, None)
372 .await?;
373 Some(session.token().to_string())
374 } else {
375 None
376 };
377
378 let response = ChangePasswordResponse {
379 token: new_token,
380 user: updated_user,
381 };
382
383 Ok(AuthResponse::json(200, &response)?)
384 }
385
386 async fn handle_set_password<DB: DatabaseAdapter>(
387 &self,
388 req: &AuthRequest,
389 ctx: &AuthContext<DB>,
390 ) -> AuthResult<AuthResponse> {
391 let set_req: SetPasswordRequest = match better_auth_core::validate_request_body(req) {
392 Ok(v) => v,
393 Err(resp) => return Ok(resp),
394 };
395
396 let user = self
398 .get_current_user(req, ctx)
399 .await?
400 .ok_or(AuthError::Unauthenticated)?;
401
402 if user
404 .metadata()
405 .get("password_hash")
406 .and_then(|v| v.as_str())
407 .is_some()
408 {
409 return Err(AuthError::bad_request(
410 "User already has a password. Use /change-password instead.",
411 ));
412 }
413
414 self.validate_password(&set_req.new_password, ctx)?;
416
417 let password_hash = self.hash_password(&set_req.new_password)?;
419
420 let mut metadata = user.metadata().clone();
421 metadata.insert(
422 "password_hash".to_string(),
423 serde_json::Value::String(password_hash),
424 );
425
426 let update_user = UpdateUser {
427 email: None,
428 name: None,
429 image: None,
430 email_verified: None,
431 username: None,
432 display_username: None,
433 role: None,
434 banned: None,
435 ban_reason: None,
436 ban_expires: None,
437 two_factor_enabled: None,
438 metadata: Some(metadata),
439 };
440
441 ctx.database.update_user(user.id(), update_user).await?;
442
443 let response = StatusResponse { status: true };
444 Ok(AuthResponse::json(200, &response)?)
445 }
446
447 async fn handle_reset_password_token<DB: DatabaseAdapter>(
448 &self,
449 token: &str,
450 _req: &AuthRequest,
451 ctx: &AuthContext<DB>,
452 ) -> AuthResult<AuthResponse> {
453 let callback_url = _req.query.get("callbackURL").cloned();
455
456 let (_user, _verification) = match self.find_user_by_reset_token(token, ctx).await? {
458 Some((user, verification)) => (user, verification),
459 None => {
460 if let Some(callback_url) = callback_url {
462 let redirect_url = format!("{}?error=INVALID_TOKEN", callback_url);
463 let mut headers = std::collections::HashMap::new();
464 headers.insert("Location".to_string(), redirect_url);
465 return Ok(AuthResponse {
466 status: 302,
467 headers,
468 body: Vec::new(),
469 });
470 }
471
472 return Err(AuthError::bad_request("Invalid or expired reset token"));
473 }
474 };
475
476 if let Some(callback_url) = callback_url {
478 let redirect_url = format!("{}?token={}", callback_url, token);
479 let mut headers = std::collections::HashMap::new();
480 headers.insert("Location".to_string(), redirect_url);
481 return Ok(AuthResponse {
482 status: 302,
483 headers,
484 body: Vec::new(),
485 });
486 }
487
488 let response = ResetPasswordTokenResponse {
490 token: token.to_string(),
491 };
492 Ok(AuthResponse::json(200, &response)?)
493 }
494
495 async fn find_user_by_reset_token<DB: DatabaseAdapter>(
496 &self,
497 token: &str,
498 ctx: &AuthContext<DB>,
499 ) -> AuthResult<Option<(DB::User, DB::Verification)>> {
500 let verification = match ctx.database.get_verification_by_value(token).await? {
502 Some(verification) => verification,
503 None => return Ok(None),
504 };
505
506 let user = match ctx
508 .database
509 .get_user_by_email(verification.identifier())
510 .await?
511 {
512 Some(user) => user,
513 None => return Ok(None),
514 };
515
516 Ok(Some((user, verification)))
517 }
518
519 async fn get_current_user<DB: DatabaseAdapter>(
520 &self,
521 req: &AuthRequest,
522 ctx: &AuthContext<DB>,
523 ) -> AuthResult<Option<DB::User>> {
524 let session_manager =
525 better_auth_core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
526
527 if let Some(token) = session_manager.extract_session_token(req)
528 && let Some(session) = session_manager.get_session(&token).await?
529 {
530 return ctx.database.get_user_by_id(session.user_id()).await;
531 }
532
533 Ok(None)
534 }
535
536 fn validate_password<DB: DatabaseAdapter>(
537 &self,
538 password: &str,
539 ctx: &AuthContext<DB>,
540 ) -> AuthResult<()> {
541 let config = &ctx.config.password;
542
543 if password.len() < config.min_length {
544 return Err(AuthError::bad_request(format!(
545 "Password must be at least {} characters long",
546 config.min_length
547 )));
548 }
549
550 if config.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
551 return Err(AuthError::bad_request(
552 "Password must contain at least one uppercase letter",
553 ));
554 }
555
556 if config.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
557 return Err(AuthError::bad_request(
558 "Password must contain at least one lowercase letter",
559 ));
560 }
561
562 if config.require_numbers && !password.chars().any(|c| c.is_ascii_digit()) {
563 return Err(AuthError::bad_request(
564 "Password must contain at least one number",
565 ));
566 }
567
568 if config.require_special
569 && !password
570 .chars()
571 .any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c))
572 {
573 return Err(AuthError::bad_request(
574 "Password must contain at least one special character",
575 ));
576 }
577
578 Ok(())
579 }
580
581 fn hash_password(&self, password: &str) -> AuthResult<String> {
582 use argon2::password_hash::{SaltString, rand_core::OsRng};
583 use argon2::{Argon2, PasswordHasher};
584
585 let salt = SaltString::generate(&mut OsRng);
586 let argon2 = Argon2::default();
587
588 let password_hash = argon2
589 .hash_password(password.as_bytes(), &salt)
590 .map_err(|e| AuthError::PasswordHash(format!("Failed to hash password: {}", e)))?;
591
592 Ok(password_hash.to_string())
593 }
594
595 fn verify_password(&self, password: &str, hash: &str) -> AuthResult<()> {
596 use argon2::password_hash::PasswordHash;
597 use argon2::{Argon2, PasswordVerifier};
598
599 let parsed_hash = PasswordHash::new(hash)
600 .map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;
601
602 let argon2 = Argon2::default();
603 argon2
604 .verify_password(password.as_bytes(), &parsed_hash)
605 .map_err(|_| AuthError::InvalidCredentials)?;
606
607 Ok(())
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use better_auth_core::adapters::{DatabaseAdapter, MemoryDatabaseAdapter};
615 use better_auth_core::config::{Argon2Config, AuthConfig, PasswordConfig};
616 use better_auth_core::{CreateSession, CreateUser, CreateVerification, Session, User};
617 use chrono::{Duration, Utc};
618 use std::collections::HashMap;
619 use std::sync::Arc;
620
621 async fn create_test_context_with_user() -> (AuthContext<MemoryDatabaseAdapter>, User, Session)
622 {
623 let mut config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
624 config.password = PasswordConfig {
625 min_length: 8,
626 require_uppercase: true,
627 require_lowercase: true,
628 require_numbers: true,
629 require_special: true,
630 argon2_config: Argon2Config::default(),
631 };
632
633 let config = Arc::new(config);
634 let database = Arc::new(MemoryDatabaseAdapter::new());
635 let ctx = AuthContext::new(config.clone(), database.clone());
636
637 let plugin = PasswordManagementPlugin::new();
639 let password_hash = plugin.hash_password("Password123!").unwrap();
640
641 let mut metadata = HashMap::new();
642 metadata.insert(
643 "password_hash".to_string(),
644 serde_json::Value::String(password_hash),
645 );
646
647 let create_user = CreateUser::new()
648 .with_email("test@example.com")
649 .with_name("Test User")
650 .with_metadata(metadata);
651 let user = database.create_user(create_user).await.unwrap();
652
653 let create_session = CreateSession {
655 user_id: user.id.clone(),
656 expires_at: Utc::now() + Duration::hours(24),
657 ip_address: Some("127.0.0.1".to_string()),
658 user_agent: Some("test-agent".to_string()),
659 impersonated_by: None,
660 active_organization_id: None,
661 };
662 let session = database.create_session(create_session).await.unwrap();
663
664 (ctx, user, session)
665 }
666
667 fn create_auth_request(
668 method: HttpMethod,
669 path: &str,
670 token: Option<&str>,
671 body: Option<Vec<u8>>,
672 ) -> AuthRequest {
673 let mut headers = HashMap::new();
674 if let Some(token) = token {
675 headers.insert("authorization".to_string(), format!("Bearer {}", token));
676 }
677
678 AuthRequest {
679 method,
680 path: path.to_string(),
681 headers,
682 body,
683 query: HashMap::new(),
684 }
685 }
686
687 #[tokio::test]
688 async fn test_forget_password_success() {
689 let plugin = PasswordManagementPlugin::new();
690 let (ctx, _user, _session) = create_test_context_with_user().await;
691
692 let body = serde_json::json!({
693 "email": "test@example.com",
694 "redirectTo": "http://localhost:3000/reset"
695 });
696
697 let req = create_auth_request(
698 HttpMethod::Post,
699 "/forget-password",
700 None,
701 Some(body.to_string().into_bytes()),
702 );
703
704 let response = plugin.handle_forget_password(&req, &ctx).await.unwrap();
705 assert_eq!(response.status, 200);
706
707 let body_str = String::from_utf8(response.body).unwrap();
708 let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
709 assert!(response_data.status);
710 }
711
712 #[tokio::test]
713 async fn test_forget_password_unknown_email() {
714 let plugin = PasswordManagementPlugin::new();
715 let (ctx, _user, _session) = create_test_context_with_user().await;
716
717 let body = serde_json::json!({
718 "email": "unknown@example.com"
719 });
720
721 let req = create_auth_request(
722 HttpMethod::Post,
723 "/forget-password",
724 None,
725 Some(body.to_string().into_bytes()),
726 );
727
728 let response = plugin.handle_forget_password(&req, &ctx).await.unwrap();
729 assert_eq!(response.status, 200);
730
731 let body_str = String::from_utf8(response.body).unwrap();
733 let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
734 assert!(response_data.status);
735 }
736
737 #[tokio::test]
738 async fn test_reset_password_success() {
739 let plugin = PasswordManagementPlugin::new();
740 let (ctx, user, _session) = create_test_context_with_user().await;
741
742 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
744 let create_verification = CreateVerification {
745 identifier: user.email.clone().unwrap(),
746 value: reset_token.clone(),
747 expires_at: Utc::now() + Duration::hours(24),
748 };
749 ctx.database
750 .create_verification(create_verification)
751 .await
752 .unwrap();
753
754 let body = serde_json::json!({
755 "newPassword": "NewPassword123!",
756 "token": reset_token
757 });
758
759 let req = create_auth_request(
760 HttpMethod::Post,
761 "/reset-password",
762 None,
763 Some(body.to_string().into_bytes()),
764 );
765
766 let response = plugin.handle_reset_password(&req, &ctx).await.unwrap();
767 assert_eq!(response.status, 200);
768
769 let body_str = String::from_utf8(response.body).unwrap();
770 let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
771 assert!(response_data.status);
772
773 let updated_user = ctx
775 .database
776 .get_user_by_id(&user.id)
777 .await
778 .unwrap()
779 .unwrap();
780 let stored_hash = updated_user
781 .metadata
782 .get("password_hash")
783 .unwrap()
784 .as_str()
785 .unwrap();
786 assert!(
787 plugin
788 .verify_password("NewPassword123!", stored_hash)
789 .is_ok()
790 );
791
792 let verification_check = ctx
794 .database
795 .get_verification_by_value(&reset_token)
796 .await
797 .unwrap();
798 assert!(verification_check.is_none());
799 }
800
801 #[tokio::test]
802 async fn test_reset_password_invalid_token() {
803 let plugin = PasswordManagementPlugin::new();
804 let (ctx, _user, _session) = create_test_context_with_user().await;
805
806 let body = serde_json::json!({
807 "newPassword": "NewPassword123!",
808 "token": "invalid_token"
809 });
810
811 let req = create_auth_request(
812 HttpMethod::Post,
813 "/reset-password",
814 None,
815 Some(body.to_string().into_bytes()),
816 );
817
818 let err = plugin.handle_reset_password(&req, &ctx).await.unwrap_err();
819 assert_eq!(err.status_code(), 400);
820 }
821
822 #[tokio::test]
823 async fn test_reset_password_weak_password() {
824 let plugin = PasswordManagementPlugin::new();
825 let (ctx, user, _session) = create_test_context_with_user().await;
826
827 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
829 let create_verification = CreateVerification {
830 identifier: user.email.clone().unwrap(),
831 value: reset_token.clone(),
832 expires_at: Utc::now() + Duration::hours(24),
833 };
834 ctx.database
835 .create_verification(create_verification)
836 .await
837 .unwrap();
838
839 let body = serde_json::json!({
840 "newPassword": "weak",
841 "token": reset_token
842 });
843
844 let req = create_auth_request(
845 HttpMethod::Post,
846 "/reset-password",
847 None,
848 Some(body.to_string().into_bytes()),
849 );
850
851 let err = plugin.handle_reset_password(&req, &ctx).await.unwrap_err();
852 assert_eq!(err.status_code(), 400);
853 }
854
855 #[tokio::test]
856 async fn test_change_password_success() {
857 let plugin = PasswordManagementPlugin::new();
858 let (ctx, _user, session) = create_test_context_with_user().await;
859
860 let body = serde_json::json!({
861 "currentPassword": "Password123!",
862 "newPassword": "NewPassword123!",
863 "revokeOtherSessions": "false"
864 });
865
866 let req = create_auth_request(
867 HttpMethod::Post,
868 "/change-password",
869 Some(&session.token),
870 Some(body.to_string().into_bytes()),
871 );
872
873 let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
874 assert_eq!(response.status, 200);
875
876 let body_str = String::from_utf8(response.body).unwrap();
877 let response_data: serde_json::Value = serde_json::from_str(&body_str).unwrap();
878 assert!(response_data["token"].is_null()); let user_id = response_data["user"]["id"].as_str().unwrap();
882 let updated_user = ctx.database.get_user_by_id(user_id).await.unwrap().unwrap();
883 let stored_hash = updated_user
884 .metadata
885 .get("password_hash")
886 .unwrap()
887 .as_str()
888 .unwrap();
889 assert!(
890 plugin
891 .verify_password("NewPassword123!", stored_hash)
892 .is_ok()
893 );
894 }
895
896 #[tokio::test]
897 async fn test_change_password_with_session_revocation() {
898 let plugin = PasswordManagementPlugin::new();
899 let (ctx, _user, session) = create_test_context_with_user().await;
900
901 let body = serde_json::json!({
902 "currentPassword": "Password123!",
903 "newPassword": "NewPassword123!",
904 "revokeOtherSessions": "true"
905 });
906
907 let req = create_auth_request(
908 HttpMethod::Post,
909 "/change-password",
910 Some(&session.token),
911 Some(body.to_string().into_bytes()),
912 );
913
914 let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
915 assert_eq!(response.status, 200);
916
917 let body_str = String::from_utf8(response.body).unwrap();
918 let response_data: serde_json::Value = serde_json::from_str(&body_str).unwrap();
919 assert!(response_data["token"].is_string()); }
921
922 #[tokio::test]
923 async fn test_change_password_wrong_current_password() {
924 let plugin = PasswordManagementPlugin::new();
925 let (ctx, _user, session) = create_test_context_with_user().await;
926
927 let body = serde_json::json!({
928 "currentPassword": "WrongPassword123!",
929 "newPassword": "NewPassword123!"
930 });
931
932 let req = create_auth_request(
933 HttpMethod::Post,
934 "/change-password",
935 Some(&session.token),
936 Some(body.to_string().into_bytes()),
937 );
938
939 let err = plugin.handle_change_password(&req, &ctx).await.unwrap_err();
940 assert_eq!(err.status_code(), 401);
941 }
942
943 #[tokio::test]
944 async fn test_change_password_unauthorized() {
945 let plugin = PasswordManagementPlugin::new();
946 let (ctx, _user, _session) = create_test_context_with_user().await;
947
948 let body = serde_json::json!({
949 "currentPassword": "Password123!",
950 "newPassword": "NewPassword123!"
951 });
952
953 let req = create_auth_request(
954 HttpMethod::Post,
955 "/change-password",
956 None,
957 Some(body.to_string().into_bytes()),
958 );
959
960 let err = plugin.handle_change_password(&req, &ctx).await.unwrap_err();
961 assert_eq!(err.status_code(), 401);
962 }
963
964 #[tokio::test]
965 async fn test_reset_password_token_endpoint_success() {
966 let plugin = PasswordManagementPlugin::new();
967 let (ctx, user, _session) = create_test_context_with_user().await;
968
969 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
971 let create_verification = CreateVerification {
972 identifier: user.email.clone().unwrap(),
973 value: reset_token.clone(),
974 expires_at: Utc::now() + Duration::hours(24),
975 };
976 ctx.database
977 .create_verification(create_verification)
978 .await
979 .unwrap();
980
981 let req = create_auth_request(HttpMethod::Get, "/reset-password/token", None, None);
982
983 let response = plugin
984 .handle_reset_password_token(&reset_token, &req, &ctx)
985 .await
986 .unwrap();
987 assert_eq!(response.status, 200);
988
989 let body_str = String::from_utf8(response.body).unwrap();
990 let response_data: ResetPasswordTokenResponse = serde_json::from_str(&body_str).unwrap();
991 assert_eq!(response_data.token, reset_token);
992 }
993
994 #[tokio::test]
995 async fn test_reset_password_token_endpoint_with_callback() {
996 let plugin = PasswordManagementPlugin::new();
997 let (ctx, user, _session) = create_test_context_with_user().await;
998
999 let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
1001 let create_verification = CreateVerification {
1002 identifier: user.email.clone().unwrap(),
1003 value: reset_token.clone(),
1004 expires_at: Utc::now() + Duration::hours(24),
1005 };
1006 ctx.database
1007 .create_verification(create_verification)
1008 .await
1009 .unwrap();
1010
1011 let mut query = HashMap::new();
1012 query.insert(
1013 "callbackURL".to_string(),
1014 "http://localhost:3000/reset".to_string(),
1015 );
1016
1017 let req = AuthRequest {
1018 method: HttpMethod::Get,
1019 path: "/reset-password/token".to_string(),
1020 headers: HashMap::new(),
1021 body: None,
1022 query,
1023 };
1024
1025 let response = plugin
1026 .handle_reset_password_token(&reset_token, &req, &ctx)
1027 .await
1028 .unwrap();
1029 assert_eq!(response.status, 302);
1030
1031 let location_header = response
1033 .headers
1034 .iter()
1035 .find(|(key, _)| *key == "Location")
1036 .map(|(_, value)| value);
1037 assert!(location_header.is_some());
1038 assert!(
1039 location_header
1040 .unwrap()
1041 .contains("http://localhost:3000/reset")
1042 );
1043 assert!(location_header.unwrap().contains(&reset_token));
1044 }
1045
1046 #[tokio::test]
1047 async fn test_reset_password_token_endpoint_invalid_token() {
1048 let plugin = PasswordManagementPlugin::new();
1049 let (ctx, _user, _session) = create_test_context_with_user().await;
1050
1051 let req = create_auth_request(HttpMethod::Get, "/reset-password/token", None, None);
1052
1053 let err = plugin
1054 .handle_reset_password_token("invalid_token", &req, &ctx)
1055 .await
1056 .unwrap_err();
1057 assert_eq!(err.status_code(), 400);
1058 }
1059
1060 #[tokio::test]
1061 async fn test_password_validation() {
1062 let plugin = PasswordManagementPlugin::new();
1063 let mut config = AuthConfig::new("test-secret");
1064 config.password = PasswordConfig {
1065 min_length: 8,
1066 require_uppercase: true,
1067 require_lowercase: true,
1068 require_numbers: true,
1069 require_special: true,
1070 argon2_config: Argon2Config::default(),
1071 };
1072 let ctx = AuthContext::new(Arc::new(config), Arc::new(MemoryDatabaseAdapter::new()));
1073
1074 assert!(plugin.validate_password("Password123!", &ctx).is_ok());
1076
1077 assert!(plugin.validate_password("Pass1!", &ctx).is_err());
1079
1080 assert!(plugin.validate_password("password123!", &ctx).is_err());
1082
1083 assert!(plugin.validate_password("PASSWORD123!", &ctx).is_err());
1085
1086 assert!(plugin.validate_password("Password!", &ctx).is_err());
1088
1089 assert!(plugin.validate_password("Password123", &ctx).is_err());
1091 }
1092
1093 #[tokio::test]
1094 async fn test_password_hashing_and_verification() {
1095 let plugin = PasswordManagementPlugin::new();
1096
1097 let password = "TestPassword123!";
1098 let hash = plugin.hash_password(password).unwrap();
1099
1100 assert!(plugin.verify_password(password, &hash).is_ok());
1102
1103 assert!(plugin.verify_password("WrongPassword123!", &hash).is_err());
1105 }
1106
1107 #[tokio::test]
1108 async fn test_plugin_routes() {
1109 let plugin = PasswordManagementPlugin::new();
1110 let routes = AuthPlugin::<MemoryDatabaseAdapter>::routes(&plugin);
1111
1112 assert_eq!(routes.len(), 5);
1113 assert!(
1114 routes
1115 .iter()
1116 .any(|r| r.path == "/forget-password" && r.method == HttpMethod::Post)
1117 );
1118 assert!(
1119 routes
1120 .iter()
1121 .any(|r| r.path == "/reset-password" && r.method == HttpMethod::Post)
1122 );
1123 assert!(
1124 routes
1125 .iter()
1126 .any(|r| r.path == "/reset-password/{token}" && r.method == HttpMethod::Get)
1127 );
1128 assert!(
1129 routes
1130 .iter()
1131 .any(|r| r.path == "/change-password" && r.method == HttpMethod::Post)
1132 );
1133 }
1134
1135 #[tokio::test]
1136 async fn test_plugin_on_request_routing() {
1137 let plugin = PasswordManagementPlugin::new();
1138 let (ctx, _user, session) = create_test_context_with_user().await;
1139
1140 let body = serde_json::json!({"email": "test@example.com"});
1142 let req = create_auth_request(
1143 HttpMethod::Post,
1144 "/forget-password",
1145 None,
1146 Some(body.to_string().into_bytes()),
1147 );
1148 let response = plugin.on_request(&req, &ctx).await.unwrap();
1149 assert!(response.is_some());
1150 assert_eq!(response.unwrap().status, 200);
1151
1152 let body = serde_json::json!({
1154 "currentPassword": "Password123!",
1155 "newPassword": "NewPassword123!"
1156 });
1157 let req = create_auth_request(
1158 HttpMethod::Post,
1159 "/change-password",
1160 Some(&session.token),
1161 Some(body.to_string().into_bytes()),
1162 );
1163 let response = plugin.on_request(&req, &ctx).await.unwrap();
1164 assert!(response.is_some());
1165 assert_eq!(response.unwrap().status, 200);
1166
1167 let req = create_auth_request(HttpMethod::Get, "/invalid-route", None, None);
1169 let response = plugin.on_request(&req, &ctx).await.unwrap();
1170 assert!(response.is_none());
1171 }
1172
1173 #[tokio::test]
1174 async fn test_configuration() {
1175 let config = PasswordManagementConfig {
1176 reset_token_expiry_hours: 48,
1177 require_current_password: false,
1178 send_email_notifications: false,
1179 };
1180
1181 let plugin = PasswordManagementPlugin::with_config(config);
1182 assert_eq!(plugin.config.reset_token_expiry_hours, 48);
1183 assert!(!plugin.config.require_current_password);
1184 assert!(!plugin.config.send_email_notifications);
1185 }
1186}