better_auth/plugins/
password_management.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use chrono::{Utc, Duration};
4use uuid::Uuid;
5
6use crate::core::{AuthPlugin, AuthRoute, AuthContext};
7use crate::types::{AuthRequest, AuthResponse, HttpMethod, User, UpdateUser, CreateVerification};
8use crate::error::{AuthError, AuthResult};
9
10/// Password management plugin for password reset and change functionality
11pub struct PasswordManagementPlugin {
12    config: PasswordManagementConfig,
13}
14
15#[derive(Debug, Clone)]
16pub struct PasswordManagementConfig {
17    pub reset_token_expiry_hours: i64,
18    pub require_current_password: bool,
19    pub send_email_notifications: bool,
20}
21
22// Request structures for password endpoints
23#[derive(Debug, Deserialize)]
24struct ForgetPasswordRequest {
25    email: String,
26    #[serde(rename = "redirectTo")]
27    redirect_to: Option<String>,
28}
29
30#[derive(Debug, Deserialize)]
31struct ResetPasswordRequest {
32    #[serde(rename = "newPassword")]
33    new_password: String,
34    token: Option<String>,
35}
36
37#[derive(Debug, Deserialize)]
38struct ChangePasswordRequest {
39    #[serde(rename = "newPassword")]
40    new_password: String,
41    #[serde(rename = "currentPassword")]
42    current_password: String,
43    #[serde(rename = "revokeOtherSessions")]
44    revoke_other_sessions: Option<String>,
45}
46
47// Response structures
48#[derive(Debug, Serialize, Deserialize)]
49struct StatusResponse {
50    status: bool,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54struct ChangePasswordResponse {
55    token: Option<String>,
56    user: User,
57}
58
59#[derive(Debug, Serialize, Deserialize)]
60struct ResetPasswordTokenResponse {
61    token: String,
62}
63
64impl PasswordManagementPlugin {
65    pub fn new() -> Self {
66        Self {
67            config: PasswordManagementConfig::default(),
68        }
69    }
70    
71    pub fn with_config(config: PasswordManagementConfig) -> Self {
72        Self { config }
73    }
74    
75    pub fn reset_token_expiry_hours(mut self, hours: i64) -> Self {
76        self.config.reset_token_expiry_hours = hours;
77        self
78    }
79    
80    pub fn require_current_password(mut self, require: bool) -> Self {
81        self.config.require_current_password = require;
82        self
83    }
84    
85    pub fn send_email_notifications(mut self, send: bool) -> Self {
86        self.config.send_email_notifications = send;
87        self
88    }
89}
90
91impl Default for PasswordManagementPlugin {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl Default for PasswordManagementConfig {
98    fn default() -> Self {
99        Self {
100            reset_token_expiry_hours: 24, // 24 hours default expiry
101            require_current_password: true,
102            send_email_notifications: true,
103        }
104    }
105}
106
107#[async_trait]
108impl AuthPlugin for PasswordManagementPlugin {
109    fn name(&self) -> &'static str {
110        "password-management"
111    }
112    
113    fn routes(&self) -> Vec<AuthRoute> {
114        vec![
115            AuthRoute::post("/forget-password", "forget_password"),
116            AuthRoute::post("/reset-password", "reset_password"),
117            AuthRoute::get("/reset-password/{token}", "reset_password_token"),
118            AuthRoute::post("/change-password", "change_password"),
119        ]
120    }
121    
122    async fn on_request(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<Option<AuthResponse>> {
123        match (req.method(), req.path()) {
124            (HttpMethod::Post, "/forget-password") => {
125                Ok(Some(self.handle_forget_password(req, ctx).await?))
126            },
127            (HttpMethod::Post, "/reset-password") => {
128                Ok(Some(self.handle_reset_password(req, ctx).await?))
129            },
130            (HttpMethod::Post, "/change-password") => {
131                Ok(Some(self.handle_change_password(req, ctx).await?))
132            },
133            (HttpMethod::Get, path) if path.starts_with("/reset-password/") => {
134                let token = &path[16..]; // Remove "/reset-password/" prefix
135                Ok(Some(self.handle_reset_password_token(token, req, ctx).await?))
136            },
137            _ => Ok(None),
138        }
139    }
140}
141
142// Implementation methods outside the trait
143impl PasswordManagementPlugin {
144    async fn handle_forget_password(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
145        let forget_req: ForgetPasswordRequest = match req.body_as_json() {
146            Ok(req) => req,
147            Err(e) => {
148                return Ok(AuthResponse::json(400, &serde_json::json!({
149                    "error": "Invalid request",
150                    "message": format!("Invalid JSON: {}", e)
151                }))?);
152            }
153        };
154        
155        // Check if user exists
156        let user = match ctx.database.get_user_by_email(&forget_req.email).await? {
157            Some(user) => user,
158            None => {
159                // Don't reveal whether email exists or not for security
160                let response = StatusResponse { status: true };
161                return Ok(AuthResponse::json(200, &response)?);
162            }
163        };
164        
165        // Generate password reset token
166        let reset_token = format!("reset_{}", Uuid::new_v4());
167        let expires_at = Utc::now() + Duration::hours(self.config.reset_token_expiry_hours);
168        
169        // Create verification token
170        let create_verification = CreateVerification {
171            identifier: user.email.clone().unwrap_or_default(),
172            value: reset_token.clone(),
173            expires_at,
174        };
175        
176        ctx.database.create_verification(create_verification).await?;
177        
178        // TODO: Send email with reset link
179        // In a real implementation, you would send an email here
180        if self.config.send_email_notifications {
181            let reset_url = if let Some(redirect_to) = &forget_req.redirect_to {
182                format!("{}?token={}", redirect_to, reset_token)
183            } else {
184                format!("{}/reset-password?token={}", ctx.config.base_url, reset_token)
185            };
186            
187            println!("Password reset email would be sent to {} with URL: {}", forget_req.email, reset_url);
188        }
189        
190        let response = StatusResponse { status: true };
191        Ok(AuthResponse::json(200, &response)?)
192    }
193    
194    async fn handle_reset_password(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
195        let reset_req: ResetPasswordRequest = match req.body_as_json() {
196            Ok(req) => req,
197            Err(e) => {
198                return Ok(AuthResponse::json(400, &serde_json::json!({
199                    "error": "Invalid request",
200                    "message": format!("Invalid JSON: {}", e)
201                }))?);
202            }
203        };
204        
205        // Validate password
206        if let Err(e) = self.validate_password(&reset_req.new_password, ctx) {
207            return Ok(AuthResponse::json(400, &serde_json::json!({
208                "error": "Invalid password",
209                "message": e.to_string()
210            }))?);
211        }
212        
213        // Find user by reset token
214        let token = reset_req.token.as_deref().unwrap_or("");
215        if token.is_empty() {
216            return Ok(AuthResponse::json(400, &serde_json::json!({
217                "error": "Invalid request",
218                "message": "Reset token is required"
219            }))?);
220        }
221        
222        let (user, verification) = match self.find_user_by_reset_token(token, ctx).await? {
223            Some((user, verification)) => (user, verification),
224            None => {
225                return Ok(AuthResponse::json(400, &serde_json::json!({
226                    "error": "Invalid token",
227                    "message": "Invalid or expired reset token"
228                }))?);
229            }
230        };
231        
232        // Hash new password
233        let password_hash = self.hash_password(&reset_req.new_password)?;
234        
235        // Update user password
236        let mut metadata = user.metadata.clone();
237        metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
238        
239        let update_user = UpdateUser {
240            email: None,
241            name: None,
242            image: None,
243            email_verified: None,
244            username: None,
245            display_username: None,
246            role: None,
247            banned: None,
248            ban_reason: None,
249            ban_expires: None,
250            two_factor_enabled: None,
251            metadata: Some(metadata),
252        };
253        
254        ctx.database.update_user(&user.id, update_user).await?;
255        
256        // Delete the used verification token
257        ctx.database.delete_verification(&verification.id).await?;
258        
259        // Revoke all existing sessions for security
260        ctx.database.delete_user_sessions(&user.id).await?;
261        
262        let response = StatusResponse { status: true };
263        Ok(AuthResponse::json(200, &response)?)
264    }
265    
266    async fn handle_change_password(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
267        let change_req: ChangePasswordRequest = match req.body_as_json() {
268            Ok(req) => req,
269            Err(e) => {
270                return Ok(AuthResponse::json(400, &serde_json::json!({
271                    "error": "Invalid request",
272                    "message": format!("Invalid JSON: {}", e)
273                }))?);
274            }
275        };
276        
277        // Get current user from session (this would normally be extracted from auth middleware)
278        // For now, we'll extract from Authorization header or session
279        let user = match self.get_current_user(req, ctx).await? {
280            Some(user) => user,
281            None => {
282                return Ok(AuthResponse::json(401, &serde_json::json!({
283                    "error": "Unauthorized",
284                    "message": "No valid session found"
285                }))?);
286            }
287        };
288        
289        // Verify current password
290        if self.config.require_current_password {
291            let stored_hash = match user.metadata.get("password_hash").and_then(|v| v.as_str()) {
292                Some(hash) => hash,
293                None => {
294                    return Ok(AuthResponse::json(400, &serde_json::json!({
295                        "error": "Invalid request",
296                        "message": "No password set for this user"
297                    }))?);
298                }
299            };
300            
301            if let Err(_) = self.verify_password(&change_req.current_password, stored_hash) {
302                return Ok(AuthResponse::json(400, &serde_json::json!({
303                    "error": "Invalid password",
304                    "message": "Current password is incorrect"
305                }))?);
306            }
307        }
308        
309        // Validate new password
310        if let Err(e) = self.validate_password(&change_req.new_password, ctx) {
311            return Ok(AuthResponse::json(400, &serde_json::json!({
312                "error": "Invalid password",
313                "message": e.to_string()
314            }))?);
315        }
316        
317        // Hash new password
318        let password_hash = self.hash_password(&change_req.new_password)?;
319        
320        // Update user password
321        let mut metadata = user.metadata.clone();
322        metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
323        
324        let update_user = UpdateUser {
325            email: None,
326            name: None,
327            image: None,
328            email_verified: None,
329            username: None,
330            display_username: None,
331            role: None,
332            banned: None,
333            ban_reason: None,
334            ban_expires: None,
335            two_factor_enabled: None,
336            metadata: Some(metadata),
337        };
338        
339        let updated_user = ctx.database.update_user(&user.id, update_user).await?;
340        
341        // Handle session revocation
342        let new_token = if change_req.revoke_other_sessions.as_deref() == Some("true") {
343            // Revoke all sessions except current one
344            ctx.database.delete_user_sessions(&user.id).await?;
345            
346            // Create new session
347            let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
348            let session = session_manager.create_session(&updated_user, None, None).await?;
349            Some(session.token)
350        } else {
351            None
352        };
353        
354        let response = ChangePasswordResponse {
355            token: new_token,
356            user: updated_user,
357        };
358        
359        Ok(AuthResponse::json(200, &response)?)
360    }
361    
362    async fn handle_reset_password_token(&self, token: &str, _req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
363        // Check if token is valid and get callback URL from query parameters
364        let callback_url = _req.query.get("callbackURL").cloned();
365        
366        // Validate the reset token exists and is not expired
367        let (_user, _verification) = match self.find_user_by_reset_token(token, ctx).await? {
368            Some((user, verification)) => (user, verification),
369            None => {
370                // Redirect to callback URL with error if provided
371                if let Some(callback_url) = callback_url {
372                    let redirect_url = format!("{}?error=INVALID_TOKEN", callback_url);
373                    let mut headers = std::collections::HashMap::new();
374                    headers.insert("Location".to_string(), redirect_url);
375                    return Ok(AuthResponse {
376                        status: 302,
377                        headers,
378                        body: Vec::new(),
379                    });
380                }
381                
382                return Ok(AuthResponse::json(400, &serde_json::json!({
383                    "error": "Invalid token",
384                    "message": "Invalid or expired reset token"
385                }))?);
386            }
387        };
388        
389        // If callback URL is provided, redirect with valid token
390        if let Some(callback_url) = callback_url {
391            let redirect_url = format!("{}?token={}", callback_url, token);
392            let mut headers = std::collections::HashMap::new();
393            headers.insert("Location".to_string(), redirect_url);
394            return Ok(AuthResponse {
395                status: 302,
396                headers,
397                body: Vec::new(),
398            });
399        }
400        
401        // Otherwise return the token directly
402        let response = ResetPasswordTokenResponse {
403            token: token.to_string(),
404        };
405        Ok(AuthResponse::json(200, &response)?)
406    }
407    
408    async fn find_user_by_reset_token(&self, token: &str, ctx: &AuthContext) -> AuthResult<Option<(User, crate::types::Verification)>> {
409        // Find verification token by value
410        let verification = match ctx.database.get_verification_by_value(token).await? {
411            Some(verification) => verification,
412            None => return Ok(None),
413        };
414        
415        // Get user by email (stored in identifier field)
416        let user = match ctx.database.get_user_by_email(&verification.identifier).await? {
417            Some(user) => user,
418            None => return Ok(None),
419        };
420        
421        Ok(Some((user, verification)))
422    }
423    
424    async fn get_current_user(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<Option<User>> {
425        // Extract session token from Authorization header or cookies
426        let token = if let Some(auth_header) = req.headers.get("authorization") {
427            if auth_header.starts_with("Bearer ") {
428                Some(&auth_header[7..])
429            } else {
430                None
431            }
432        } else {
433            None
434        };
435        
436        if let Some(token) = token {
437            let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
438            if let Some(session) = session_manager.get_session(token).await? {
439                return ctx.database.get_user_by_id(&session.user_id).await;
440            }
441        }
442        
443        Ok(None)
444    }
445    
446    fn validate_password(&self, password: &str, ctx: &AuthContext) -> AuthResult<()> {
447        let config = &ctx.config.password;
448        
449        if password.len() < config.min_length {
450            return Err(AuthError::InvalidRequest(format!(
451                "Password must be at least {} characters long",
452                config.min_length
453            )));
454        }
455        
456        if config.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
457            return Err(AuthError::InvalidRequest(
458                "Password must contain at least one uppercase letter".to_string()
459            ));
460        }
461        
462        if config.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
463            return Err(AuthError::InvalidRequest(
464                "Password must contain at least one lowercase letter".to_string()
465            ));
466        }
467        
468        if config.require_numbers && !password.chars().any(|c| c.is_ascii_digit()) {
469            return Err(AuthError::InvalidRequest(
470                "Password must contain at least one number".to_string()
471            ));
472        }
473        
474        if config.require_special && !password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)) {
475            return Err(AuthError::InvalidRequest(
476                "Password must contain at least one special character".to_string()
477            ));
478        }
479        
480        Ok(())
481    }
482    
483    fn hash_password(&self, password: &str) -> AuthResult<String> {
484        use argon2::{Argon2, PasswordHasher};
485        use argon2::password_hash::{SaltString, rand_core::OsRng};
486        
487        let salt = SaltString::generate(&mut OsRng);
488        let argon2 = Argon2::default();
489        
490        let password_hash = argon2.hash_password(password.as_bytes(), &salt)
491            .map_err(|e| AuthError::PasswordHash(format!("Failed to hash password: {}", e)))?;
492            
493        Ok(password_hash.to_string())
494    }
495    
496    fn verify_password(&self, password: &str, hash: &str) -> AuthResult<()> {
497        use argon2::{Argon2, PasswordVerifier};
498        use argon2::password_hash::PasswordHash;
499        
500        let parsed_hash = PasswordHash::new(hash)
501            .map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;
502            
503        let argon2 = Argon2::default();
504        argon2.verify_password(password.as_bytes(), &parsed_hash)
505            .map_err(|_| AuthError::InvalidCredentials)?;
506            
507        Ok(())
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514    use crate::core::config::{AuthConfig, PasswordConfig, Argon2Config};
515    use crate::adapters::{MemoryDatabaseAdapter, DatabaseAdapter};
516    use crate::types::{CreateUser, CreateSession, CreateVerification, Session, User};
517    use chrono::{Utc, Duration};
518    use std::collections::HashMap;
519    use std::sync::Arc;
520    
521    async fn create_test_context_with_user() -> (AuthContext, User, Session) {
522        let mut config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
523        config.password = PasswordConfig {
524            min_length: 8,
525            require_uppercase: true,
526            require_lowercase: true,
527            require_numbers: true,
528            require_special: true,
529            argon2_config: Argon2Config::default(),
530        };
531        
532        let config = Arc::new(config);
533        let database = Arc::new(MemoryDatabaseAdapter::new());
534        let ctx = AuthContext::new(config.clone(), database.clone());
535        
536        // Create test user with hashed password
537        let plugin = PasswordManagementPlugin::new();
538        let password_hash = plugin.hash_password("Password123!").unwrap();
539        
540        let mut metadata = HashMap::new();
541        metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
542        
543        let create_user = CreateUser::new()
544            .with_email("test@example.com")
545            .with_name("Test User")
546            .with_metadata(metadata);
547        let user = database.create_user(create_user).await.unwrap();
548        
549        // Create test session
550        let create_session = CreateSession {
551            user_id: user.id.clone(),
552            expires_at: Utc::now() + Duration::hours(24),
553            ip_address: Some("127.0.0.1".to_string()),
554            user_agent: Some("test-agent".to_string()),
555            impersonated_by: None,
556            active_organization_id: None,
557        };
558        let session = database.create_session(create_session).await.unwrap();
559        
560        (ctx, user, session)
561    }
562    
563    fn create_auth_request(method: HttpMethod, path: &str, token: Option<&str>, body: Option<Vec<u8>>) -> AuthRequest {
564        let mut headers = HashMap::new();
565        if let Some(token) = token {
566            headers.insert("authorization".to_string(), format!("Bearer {}", token));
567        }
568        
569        AuthRequest {
570            method,
571            path: path.to_string(),
572            headers,
573            body,
574            query: HashMap::new(),
575        }
576    }
577    
578    #[tokio::test]
579    async fn test_forget_password_success() {
580        let plugin = PasswordManagementPlugin::new();
581        let (ctx, _user, _session) = create_test_context_with_user().await;
582        
583        let body = serde_json::json!({
584            "email": "test@example.com",
585            "redirectTo": "http://localhost:3000/reset"
586        });
587        
588        let req = create_auth_request(
589            HttpMethod::Post,
590            "/forget-password",
591            None,
592            Some(body.to_string().into_bytes())
593        );
594        
595        let response = plugin.handle_forget_password(&req, &ctx).await.unwrap();
596        assert_eq!(response.status, 200);
597        
598        let body_str = String::from_utf8(response.body).unwrap();
599        let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
600        assert!(response_data.status);
601    }
602    
603    #[tokio::test]
604    async fn test_forget_password_unknown_email() {
605        let plugin = PasswordManagementPlugin::new();
606        let (ctx, _user, _session) = create_test_context_with_user().await;
607        
608        let body = serde_json::json!({
609            "email": "unknown@example.com"
610        });
611        
612        let req = create_auth_request(
613            HttpMethod::Post,
614            "/forget-password",
615            None,
616            Some(body.to_string().into_bytes())
617        );
618        
619        let response = plugin.handle_forget_password(&req, &ctx).await.unwrap();
620        assert_eq!(response.status, 200);
621        
622        // Should return success even for unknown emails (security)
623        let body_str = String::from_utf8(response.body).unwrap();
624        let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
625        assert!(response_data.status);
626    }
627    
628    #[tokio::test]
629    async fn test_reset_password_success() {
630        let plugin = PasswordManagementPlugin::new();
631        let (ctx, user, _session) = create_test_context_with_user().await;
632        
633        // Create verification token
634        let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
635        let create_verification = CreateVerification {
636            identifier: user.email.clone().unwrap(),
637            value: reset_token.clone(),
638            expires_at: Utc::now() + Duration::hours(24),
639        };
640        ctx.database.create_verification(create_verification).await.unwrap();
641        
642        let body = serde_json::json!({
643            "newPassword": "NewPassword123!",
644            "token": reset_token
645        });
646        
647        let req = create_auth_request(
648            HttpMethod::Post,
649            "/reset-password",
650            None,
651            Some(body.to_string().into_bytes())
652        );
653        
654        let response = plugin.handle_reset_password(&req, &ctx).await.unwrap();
655        assert_eq!(response.status, 200);
656        
657        let body_str = String::from_utf8(response.body).unwrap();
658        let response_data: StatusResponse = serde_json::from_str(&body_str).unwrap();
659        assert!(response_data.status);
660        
661        // Verify password was updated
662        let updated_user = ctx.database.get_user_by_id(&user.id).await.unwrap().unwrap();
663        let stored_hash = updated_user.metadata.get("password_hash").unwrap().as_str().unwrap();
664        assert!(plugin.verify_password("NewPassword123!", stored_hash).is_ok());
665        
666        // Verify token was deleted
667        let verification_check = ctx.database.get_verification_by_value(&reset_token).await.unwrap();
668        assert!(verification_check.is_none());
669    }
670    
671    #[tokio::test]
672    async fn test_reset_password_invalid_token() {
673        let plugin = PasswordManagementPlugin::new();
674        let (ctx, _user, _session) = create_test_context_with_user().await;
675        
676        let body = serde_json::json!({
677            "newPassword": "NewPassword123!",
678            "token": "invalid_token"
679        });
680        
681        let req = create_auth_request(
682            HttpMethod::Post,
683            "/reset-password",
684            None,
685            Some(body.to_string().into_bytes())
686        );
687        
688        let response = plugin.handle_reset_password(&req, &ctx).await.unwrap();
689        assert_eq!(response.status, 400);
690    }
691    
692    #[tokio::test]
693    async fn test_reset_password_weak_password() {
694        let plugin = PasswordManagementPlugin::new();
695        let (ctx, user, _session) = create_test_context_with_user().await;
696        
697        // Create verification token
698        let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
699        let create_verification = CreateVerification {
700            identifier: user.email.clone().unwrap(),
701            value: reset_token.clone(),
702            expires_at: Utc::now() + Duration::hours(24),
703        };
704        ctx.database.create_verification(create_verification).await.unwrap();
705        
706        let body = serde_json::json!({
707            "newPassword": "weak",
708            "token": reset_token
709        });
710        
711        let req = create_auth_request(
712            HttpMethod::Post,
713            "/reset-password",
714            None,
715            Some(body.to_string().into_bytes())
716        );
717        
718        let response = plugin.handle_reset_password(&req, &ctx).await.unwrap();
719        assert_eq!(response.status, 400);
720    }
721    
722    #[tokio::test]
723    async fn test_change_password_success() {
724        let plugin = PasswordManagementPlugin::new();
725        let (ctx, _user, session) = create_test_context_with_user().await;
726        
727        let body = serde_json::json!({
728            "currentPassword": "Password123!",
729            "newPassword": "NewPassword123!",
730            "revokeOtherSessions": "false"
731        });
732        
733        let req = create_auth_request(
734            HttpMethod::Post,
735            "/change-password",
736            Some(&session.token),
737            Some(body.to_string().into_bytes())
738        );
739        
740        let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
741        assert_eq!(response.status, 200);
742        
743        let body_str = String::from_utf8(response.body).unwrap();
744        let response_data: ChangePasswordResponse = serde_json::from_str(&body_str).unwrap();
745        assert!(response_data.token.is_none()); // No new token when not revoking sessions
746        
747        // Verify password was updated by checking the database directly
748        let updated_user = ctx.database.get_user_by_id(&response_data.user.id).await.unwrap().unwrap();
749        let stored_hash = updated_user.metadata.get("password_hash").unwrap().as_str().unwrap();
750        assert!(plugin.verify_password("NewPassword123!", stored_hash).is_ok());
751    }
752    
753    #[tokio::test]
754    async fn test_change_password_with_session_revocation() {
755        let plugin = PasswordManagementPlugin::new();
756        let (ctx, _user, session) = create_test_context_with_user().await;
757        
758        let body = serde_json::json!({
759            "currentPassword": "Password123!",
760            "newPassword": "NewPassword123!",
761            "revokeOtherSessions": "true"
762        });
763        
764        let req = create_auth_request(
765            HttpMethod::Post,
766            "/change-password",
767            Some(&session.token),
768            Some(body.to_string().into_bytes())
769        );
770        
771        let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
772        assert_eq!(response.status, 200);
773        
774        let body_str = String::from_utf8(response.body).unwrap();
775        let response_data: ChangePasswordResponse = serde_json::from_str(&body_str).unwrap();
776        assert!(response_data.token.is_some()); // New token when revoking sessions
777    }
778    
779    #[tokio::test]
780    async fn test_change_password_wrong_current_password() {
781        let plugin = PasswordManagementPlugin::new();
782        let (ctx, _user, session) = create_test_context_with_user().await;
783        
784        let body = serde_json::json!({
785            "currentPassword": "WrongPassword123!",
786            "newPassword": "NewPassword123!"
787        });
788        
789        let req = create_auth_request(
790            HttpMethod::Post,
791            "/change-password",
792            Some(&session.token),
793            Some(body.to_string().into_bytes())
794        );
795        
796        let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
797        assert_eq!(response.status, 400);
798    }
799    
800    #[tokio::test]
801    async fn test_change_password_unauthorized() {
802        let plugin = PasswordManagementPlugin::new();
803        let (ctx, _user, _session) = create_test_context_with_user().await;
804        
805        let body = serde_json::json!({
806            "currentPassword": "Password123!",
807            "newPassword": "NewPassword123!"
808        });
809        
810        let req = create_auth_request(
811            HttpMethod::Post,
812            "/change-password",
813            None,
814            Some(body.to_string().into_bytes())
815        );
816        
817        let response = plugin.handle_change_password(&req, &ctx).await.unwrap();
818        assert_eq!(response.status, 401);
819    }
820    
821    #[tokio::test]
822    async fn test_reset_password_token_endpoint_success() {
823        let plugin = PasswordManagementPlugin::new();
824        let (ctx, user, _session) = create_test_context_with_user().await;
825        
826        // Create verification token
827        let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
828        let create_verification = CreateVerification {
829            identifier: user.email.clone().unwrap(),
830            value: reset_token.clone(),
831            expires_at: Utc::now() + Duration::hours(24),
832        };
833        ctx.database.create_verification(create_verification).await.unwrap();
834        
835        let req = create_auth_request(HttpMethod::Get, "/reset-password/token", None, None);
836        
837        let response = plugin.handle_reset_password_token(&reset_token, &req, &ctx).await.unwrap();
838        assert_eq!(response.status, 200);
839        
840        let body_str = String::from_utf8(response.body).unwrap();
841        let response_data: ResetPasswordTokenResponse = serde_json::from_str(&body_str).unwrap();
842        assert_eq!(response_data.token, reset_token);
843    }
844    
845    #[tokio::test]
846    async fn test_reset_password_token_endpoint_with_callback() {
847        let plugin = PasswordManagementPlugin::new();
848        let (ctx, user, _session) = create_test_context_with_user().await;
849        
850        // Create verification token
851        let reset_token = format!("reset_{}", uuid::Uuid::new_v4());
852        let create_verification = CreateVerification {
853            identifier: user.email.clone().unwrap(),
854            value: reset_token.clone(),
855            expires_at: Utc::now() + Duration::hours(24),
856        };
857        ctx.database.create_verification(create_verification).await.unwrap();
858        
859        let mut query = HashMap::new();
860        query.insert("callbackURL".to_string(), "http://localhost:3000/reset".to_string());
861        
862        let req = AuthRequest {
863            method: HttpMethod::Get,
864            path: "/reset-password/token".to_string(),
865            headers: HashMap::new(),
866            body: None,
867            query,
868        };
869        
870        let response = plugin.handle_reset_password_token(&reset_token, &req, &ctx).await.unwrap();
871        assert_eq!(response.status, 302);
872        
873        // Check redirect URL
874        let location_header = response.headers.iter()
875            .find(|(key, _)| *key == "Location")
876            .map(|(_, value)| value);
877        assert!(location_header.is_some());
878        assert!(location_header.unwrap().contains("http://localhost:3000/reset"));
879        assert!(location_header.unwrap().contains(&reset_token));
880    }
881    
882    #[tokio::test]
883    async fn test_reset_password_token_endpoint_invalid_token() {
884        let plugin = PasswordManagementPlugin::new();
885        let (ctx, _user, _session) = create_test_context_with_user().await;
886        
887        let req = create_auth_request(HttpMethod::Get, "/reset-password/token", None, None);
888        
889        let response = plugin.handle_reset_password_token("invalid_token", &req, &ctx).await.unwrap();
890        assert_eq!(response.status, 400);
891    }
892    
893    #[tokio::test]
894    async fn test_password_validation() {
895        let plugin = PasswordManagementPlugin::new();
896        let mut config = AuthConfig::new("test-secret");
897        config.password = PasswordConfig {
898            min_length: 8,
899            require_uppercase: true,
900            require_lowercase: true,
901            require_numbers: true,
902            require_special: true,
903            argon2_config: Argon2Config::default(),
904        };
905        let ctx = AuthContext::new(Arc::new(config), Arc::new(MemoryDatabaseAdapter::new()));
906        
907        // Test valid password
908        assert!(plugin.validate_password("Password123!", &ctx).is_ok());
909        
910        // Test too short
911        assert!(plugin.validate_password("Pass1!", &ctx).is_err());
912        
913        // Test missing uppercase
914        assert!(plugin.validate_password("password123!", &ctx).is_err());
915        
916        // Test missing lowercase
917        assert!(plugin.validate_password("PASSWORD123!", &ctx).is_err());
918        
919        // Test missing number
920        assert!(plugin.validate_password("Password!", &ctx).is_err());
921        
922        // Test missing special character
923        assert!(plugin.validate_password("Password123", &ctx).is_err());
924    }
925    
926    #[tokio::test]
927    async fn test_password_hashing_and_verification() {
928        let plugin = PasswordManagementPlugin::new();
929        
930        let password = "TestPassword123!";
931        let hash = plugin.hash_password(password).unwrap();
932        
933        // Should verify correctly
934        assert!(plugin.verify_password(password, &hash).is_ok());
935        
936        // Should fail with wrong password
937        assert!(plugin.verify_password("WrongPassword123!", &hash).is_err());
938    }
939    
940    #[tokio::test]
941    async fn test_plugin_routes() {
942        let plugin = PasswordManagementPlugin::new();
943        let routes = plugin.routes();
944        
945        assert_eq!(routes.len(), 4);
946        assert!(routes.iter().any(|r| r.path == "/forget-password" && r.method == HttpMethod::Post));
947        assert!(routes.iter().any(|r| r.path == "/reset-password" && r.method == HttpMethod::Post));
948        assert!(routes.iter().any(|r| r.path == "/reset-password/{token}" && r.method == HttpMethod::Get));
949        assert!(routes.iter().any(|r| r.path == "/change-password" && r.method == HttpMethod::Post));
950    }
951    
952    #[tokio::test]
953    async fn test_plugin_on_request_routing() {
954        let plugin = PasswordManagementPlugin::new();
955        let (ctx, _user, session) = create_test_context_with_user().await;
956        
957        // Test forget password
958        let body = serde_json::json!({"email": "test@example.com"});
959        let req = create_auth_request(
960            HttpMethod::Post,
961            "/forget-password",
962            None,
963            Some(body.to_string().into_bytes())
964        );
965        let response = plugin.on_request(&req, &ctx).await.unwrap();
966        assert!(response.is_some());
967        assert_eq!(response.unwrap().status, 200);
968        
969        // Test change password
970        let body = serde_json::json!({
971            "currentPassword": "Password123!",
972            "newPassword": "NewPassword123!"
973        });
974        let req = create_auth_request(
975            HttpMethod::Post,
976            "/change-password",
977            Some(&session.token),
978            Some(body.to_string().into_bytes())
979        );
980        let response = plugin.on_request(&req, &ctx).await.unwrap();
981        assert!(response.is_some());
982        assert_eq!(response.unwrap().status, 200);
983        
984        // Test invalid route
985        let req = create_auth_request(HttpMethod::Get, "/invalid-route", None, None);
986        let response = plugin.on_request(&req, &ctx).await.unwrap();
987        assert!(response.is_none());
988    }
989    
990    #[tokio::test]
991    async fn test_configuration() {
992        let config = PasswordManagementConfig {
993            reset_token_expiry_hours: 48,
994            require_current_password: false,
995            send_email_notifications: false,
996        };
997        
998        let plugin = PasswordManagementPlugin::with_config(config);
999        assert_eq!(plugin.config.reset_token_expiry_hours, 48);
1000        assert!(!plugin.config.require_current_password);
1001        assert!(!plugin.config.send_email_notifications);
1002    }
1003}