better_auth/plugins/
email_password.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
4use argon2::password_hash::{SaltString, rand_core::OsRng};
5
6use crate::core::{AuthPlugin, AuthRoute, AuthContext};
7use crate::types::{AuthRequest, AuthResponse, HttpMethod, CreateUser, User};
8use crate::error::{AuthError, AuthResult};
9
10/// Email and password authentication plugin
11pub struct EmailPasswordPlugin {
12    config: EmailPasswordConfig,
13}
14
15#[derive(Debug, Clone)]
16pub struct EmailPasswordConfig {
17    pub enable_signup: bool,
18    pub require_email_verification: bool,
19    pub password_min_length: usize,
20}
21
22#[derive(Debug, Deserialize)]
23struct SignUpRequest {
24    name: String,
25    email: String,
26    password: String,
27    #[serde(rename = "callbackURL")]
28    callback_url: Option<String>,
29}
30
31#[derive(Debug, Deserialize)]
32struct SignInRequest {
33    email: String,
34    password: String,
35    #[serde(rename = "callbackURL")]
36    callback_url: Option<String>,
37    #[serde(rename = "rememberMe")]
38    remember_me: Option<bool>,
39}
40
41#[derive(Debug, Serialize)]
42struct SignUpResponse {
43    token: Option<String>,
44    user: User,
45}
46
47#[derive(Debug, Serialize)]
48struct SignInResponse {
49    redirect: bool,
50    token: String,
51    url: Option<String>,
52    user: User,
53}
54
55impl EmailPasswordPlugin {
56    pub fn new() -> Self {
57        Self {
58            config: EmailPasswordConfig::default(),
59        }
60    }
61    
62    pub fn with_config(config: EmailPasswordConfig) -> Self {
63        Self { config }
64    }
65    
66    pub fn enable_signup(mut self, enable: bool) -> Self {
67        self.config.enable_signup = enable;
68        self
69    }
70    
71    pub fn require_email_verification(mut self, require: bool) -> Self {
72        self.config.require_email_verification = require;
73        self
74    }
75    
76    pub fn password_min_length(mut self, length: usize) -> Self {
77        self.config.password_min_length = length;
78        self
79    }
80    
81    async fn handle_sign_up(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
82        if !self.config.enable_signup {
83            return Ok(AuthResponse::json(403, &serde_json::json!({
84                "error": "Signup disabled",
85                "message": "User registration is not enabled"
86            }))?);
87        }
88        
89        let signup_req: SignUpRequest = match req.body_as_json() {
90            Ok(req) => req,
91            Err(e) => {
92                return Ok(AuthResponse::json(400, &serde_json::json!({
93                    "error": "Invalid request",
94                    "message": format!("Invalid JSON: {}", e)
95                }))?);
96            }
97        };
98        
99        // Validate password
100        if let Err(e) = self.validate_password(&signup_req.password, ctx) {
101            return Ok(AuthResponse::json(400, &serde_json::json!({
102                "error": "Invalid request", 
103                "message": e.to_string()
104            }))?);
105        }
106        
107        // Check if user already exists
108        if let Some(_) = ctx.database.get_user_by_email(&signup_req.email).await? {
109            return Ok(AuthResponse::json(409, &serde_json::json!({
110                "error": "User exists",
111                "message": "A user with this email already exists"
112            }))?);
113        }
114        
115        // Hash password
116        let password_hash = self.hash_password(&signup_req.password)?;
117        
118        // Create user with password hash in metadata
119        let mut metadata = std::collections::HashMap::new();
120        metadata.insert("password_hash".to_string(), serde_json::Value::String(password_hash));
121        
122        let create_user = CreateUser::new()
123            .with_email(&signup_req.email)
124            .with_name(&signup_req.name);
125        
126        let mut create_user = create_user;
127        create_user.metadata = Some(metadata);
128            
129        let user = ctx.database.create_user(create_user).await?;
130        
131        // Create session
132        let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
133        let session = session_manager.create_session(&user, None, None).await?;
134        
135        let response = SignUpResponse {
136            token: Some(session.token.clone()),
137            user,
138        };
139        
140        // Create session cookie
141        let cookie_header = self.create_session_cookie(&session.token, ctx);
142        
143        Ok(AuthResponse::json(200, &response)?
144            .with_header("Set-Cookie", cookie_header))
145    }
146    
147    async fn handle_sign_in(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<AuthResponse> {
148        let signin_req: SignInRequest = match req.body_as_json() {
149            Ok(req) => req,
150            Err(e) => {
151                return Ok(AuthResponse::json(400, &serde_json::json!({
152                    "error": "Invalid request",
153                    "message": format!("Invalid JSON: {}", e)
154                }))?);
155            }
156        };
157        
158        // Get user by email
159        let user = match ctx.database.get_user_by_email(&signin_req.email).await? {
160            Some(user) => user,
161            None => {
162                return Ok(AuthResponse::json(401, &serde_json::json!({
163                    "error": "Invalid credentials",
164                    "message": "Email or password is incorrect"
165                }))?);
166            }
167        };
168        
169        // Verify password (assuming password is stored in metadata)
170        let stored_hash = match user.metadata.get("password_hash").and_then(|v| v.as_str()) {
171            Some(hash) => hash,
172            None => {
173                return Ok(AuthResponse::json(401, &serde_json::json!({
174                    "error": "Invalid credentials",
175                    "message": "Email or password is incorrect"
176                }))?);
177            }
178        };
179            
180        if let Err(_) = self.verify_password(&signin_req.password, stored_hash) {
181            return Ok(AuthResponse::json(401, &serde_json::json!({
182                "error": "Invalid credentials",
183                "message": "Email or password is incorrect"
184            }))?);
185        }
186        
187        // Create session
188        let session_manager = crate::core::SessionManager::new(ctx.config.clone(), ctx.database.clone());
189        let session = session_manager.create_session(&user, None, None).await?;
190        
191        let response = SignInResponse {
192            redirect: false,
193            token: session.token.clone(),
194            url: None,
195            user,
196        };
197        
198        // Create session cookie
199        let cookie_header = self.create_session_cookie(&session.token, ctx);
200        
201        Ok(AuthResponse::json(200, &response)?
202            .with_header("Set-Cookie", cookie_header))
203    }
204    
205    fn validate_password(&self, password: &str, ctx: &AuthContext) -> AuthResult<()> {
206        if password.len() < ctx.config.password.min_length {
207            return Err(AuthError::InvalidRequest(format!(
208                "Password must be at least {} characters long",
209                ctx.config.password.min_length
210            )));
211        }
212        
213        // Add more password validation rules here
214        
215        Ok(())
216    }
217    
218    fn hash_password(&self, password: &str) -> AuthResult<String> {
219        let salt = SaltString::generate(&mut OsRng);
220        let argon2 = Argon2::default();
221        
222        let password_hash = argon2.hash_password(password.as_bytes(), &salt)
223            .map_err(|e| AuthError::PasswordHash(format!("Failed to hash password: {}", e)))?;
224            
225        Ok(password_hash.to_string())
226    }
227    
228    fn create_session_cookie(&self, token: &str, ctx: &AuthContext) -> String {
229        let session_config = &ctx.config.session;
230        let secure = if session_config.cookie_secure { "; Secure" } else { "" };
231        let http_only = if session_config.cookie_http_only { "; HttpOnly" } else { "" };
232        let same_site = match session_config.cookie_same_site {
233            crate::core::config::SameSite::Strict => "; SameSite=Strict",
234            crate::core::config::SameSite::Lax => "; SameSite=Lax", 
235            crate::core::config::SameSite::None => "; SameSite=None",
236        };
237        
238        // Set expiration based on session config
239        let expires = chrono::Utc::now() + session_config.expires_in;
240        let expires_str = expires.format("%a, %d %b %Y %H:%M:%S GMT");
241        
242        format!("{}={}; Path=/; Expires={}{}{}{}",
243                session_config.cookie_name,
244                token,
245                expires_str,
246                secure,
247                http_only,
248                same_site)
249    }
250    
251    fn verify_password(&self, password: &str, hash: &str) -> AuthResult<()> {
252        let parsed_hash = PasswordHash::new(hash)
253            .map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;
254            
255        let argon2 = Argon2::default();
256        argon2.verify_password(password.as_bytes(), &parsed_hash)
257            .map_err(|_| AuthError::InvalidCredentials)?;
258            
259        Ok(())
260    }
261}
262
263impl Default for EmailPasswordConfig {
264    fn default() -> Self {
265        Self {
266            enable_signup: true,
267            require_email_verification: false,
268            password_min_length: 8,
269        }
270    }
271}
272
273#[async_trait]
274impl AuthPlugin for EmailPasswordPlugin {
275    fn name(&self) -> &'static str {
276        "email-password"
277    }
278    
279    fn routes(&self) -> Vec<AuthRoute> {
280        let mut routes = vec![
281            AuthRoute::post("/sign-in/email", "sign_in_email"),
282        ];
283        
284        if self.config.enable_signup {
285            routes.push(AuthRoute::post("/sign-up/email", "sign_up_email"));
286        }
287        
288        routes
289    }
290    
291    async fn on_request(&self, req: &AuthRequest, ctx: &AuthContext) -> AuthResult<Option<crate::types::AuthResponse>> {
292        match (req.method(), req.path()) {
293            (HttpMethod::Post, "/sign-up/email") if self.config.enable_signup => {
294                Ok(Some(self.handle_sign_up(req, ctx).await?))
295            },
296            (HttpMethod::Post, "/sign-in/email") => {
297                Ok(Some(self.handle_sign_in(req, ctx).await?))
298            },
299            _ => Ok(None),
300        }
301    }
302    
303    async fn on_user_created(&self, user: &User, ctx: &AuthContext) -> AuthResult<()> {
304        // Send verification email if required
305        if self.config.require_email_verification && !user.email_verified {
306            if let Some(email) = &user.email {
307                println!("📧 Email verification required for user: {}", email);
308                // The email verification plugin will handle sending the email
309                // via its on_user_created hook
310            }
311        }
312        
313        Ok(())
314    }
315}