better_auth_api/plugins/password_management/
mod.rs1use async_trait::async_trait;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use better_auth_core::AuthSession;
7use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
8use better_auth_core::{AuthError, AuthResult};
9use better_auth_core::{AuthRequest, AuthResponse, HttpMethod};
10
11use better_auth_core::RequestMeta;
12use better_auth_core::utils::password::PasswordHasher;
13
14use super::StatusResponse;
15
16pub(super) mod handlers;
17pub(super) mod types;
18
19#[cfg(test)]
20mod tests;
21
22use handlers::*;
23use types::*;
24
25pub type OnPasswordResetCallback =
27 dyn Fn(serde_json::Value) -> Pin<Box<dyn Future<Output = AuthResult<()>> + Send>> + Send + Sync;
28
29#[async_trait]
35pub trait SendResetPassword: Send + Sync {
36 async fn send(&self, user: &serde_json::Value, url: &str, token: &str) -> AuthResult<()>;
42}
43
44pub struct PasswordManagementPlugin {
46 config: PasswordManagementConfig,
47}
48
49#[derive(Clone, better_auth_core::PluginConfig)]
50#[plugin(name = "PasswordManagementPlugin")]
51pub struct PasswordManagementConfig {
52 #[config(default = 24)]
53 pub reset_token_expiry_hours: i64,
54 #[config(default = true)]
55 pub require_current_password: bool,
56 #[config(default = true)]
57 pub send_email_notifications: bool,
58 #[config(default = false)]
60 pub revoke_sessions_on_password_reset: bool,
61 #[config(default = None)]
64 pub send_reset_password: Option<Arc<dyn SendResetPassword>>,
65 #[config(default = None)]
68 pub on_password_reset: Option<Arc<OnPasswordResetCallback>>,
69 #[config(default = None)]
71 pub password_hasher: Option<Arc<dyn PasswordHasher>>,
72}
73
74impl std::fmt::Debug for PasswordManagementConfig {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("PasswordManagementConfig")
77 .field("reset_token_expiry_hours", &self.reset_token_expiry_hours)
78 .field("require_current_password", &self.require_current_password)
79 .field("send_email_notifications", &self.send_email_notifications)
80 .field(
81 "revoke_sessions_on_password_reset",
82 &self.revoke_sessions_on_password_reset,
83 )
84 .field(
85 "send_reset_password",
86 &self.send_reset_password.as_ref().map(|_| "custom"),
87 )
88 .field(
89 "on_password_reset",
90 &self.on_password_reset.as_ref().map(|_| "custom"),
91 )
92 .field(
93 "password_hasher",
94 &self.password_hasher.as_ref().map(|_| "custom"),
95 )
96 .finish()
97 }
98}
99
100#[async_trait]
101impl<S: better_auth_core::AuthSchema> AuthPlugin<S> for PasswordManagementPlugin {
102 fn name(&self) -> &'static str {
103 "password-management"
104 }
105
106 fn routes(&self) -> Vec<AuthRoute> {
107 vec![
108 AuthRoute::post("/request-password-reset", "request_password_reset"),
109 AuthRoute::post("/reset-password", "reset_password"),
110 AuthRoute::get("/reset-password/{token}", "reset_password_token"),
111 AuthRoute::post("/change-password", "change_password"),
112 AuthRoute::post("/verify-password", "verify_password"),
113 ]
114 }
115
116 async fn on_request(
117 &self,
118 req: &AuthRequest,
119 ctx: &AuthContext<S>,
120 ) -> AuthResult<Option<AuthResponse>> {
121 match (req.method(), req.path()) {
122 (HttpMethod::Post, "/request-password-reset") => {
123 Ok(Some(self.handle_request_password_reset(req, ctx).await?))
124 }
125 (HttpMethod::Post, "/reset-password") => {
126 Ok(Some(self.handle_reset_password(req, ctx).await?))
127 }
128 (HttpMethod::Post, "/change-password") => {
129 Ok(Some(self.handle_change_password(req, ctx).await?))
130 }
131 (HttpMethod::Post, "/verify-password") => {
132 Ok(Some(self.handle_verify_password(req, ctx).await?))
133 }
134 (HttpMethod::Get, path) if path.starts_with("/reset-password/") => {
135 let token = path.get(16..).unwrap_or(""); Ok(Some(
137 self.handle_reset_password_token(token, req, ctx).await?,
138 ))
139 }
140 _ => Ok(None),
141 }
142 }
143}
144
145impl PasswordManagementPlugin {
147 async fn handle_request_password_reset(
148 &self,
149 req: &AuthRequest,
150 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
151 ) -> AuthResult<AuthResponse> {
152 let body: RequestPasswordResetRequest = match better_auth_core::validate_request_body(req) {
153 Ok(v) => v,
154 Err(resp) => return Ok(resp),
155 };
156 let response = request_password_reset_core(&body, &self.config, ctx).await?;
157 Ok(AuthResponse::json(200, &response)?)
158 }
159
160 async fn handle_reset_password(
161 &self,
162 req: &AuthRequest,
163 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
164 ) -> AuthResult<AuthResponse> {
165 let mut body: ResetPasswordRequest = match better_auth_core::validate_request_body(req) {
166 Ok(v) => v,
167 Err(resp) => return Ok(resp),
168 };
169 if body.token.is_none() {
170 body.token = req.query.get("token").cloned();
171 }
172 let response = reset_password_core(&body, &self.config, ctx).await?;
173 Ok(AuthResponse::json(200, &response)?)
174 }
175
176 async fn handle_change_password(
177 &self,
178 req: &AuthRequest,
179 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
180 ) -> AuthResult<AuthResponse> {
181 let body: ChangePasswordRequest = match better_auth_core::validate_request_body(req) {
182 Ok(v) => v,
183 Err(resp) => return Ok(resp),
184 };
185
186 let user = self
188 .get_current_user(req, ctx)
189 .await?
190 .ok_or(AuthError::Unauthenticated)?;
191 let meta = RequestMeta::from_request(req);
192
193 let (response, new_token) =
194 change_password_core(&body, &user, &self.config, &meta, ctx).await?;
195
196 let auth_response = AuthResponse::json(200, &response)?;
197
198 if let Some(token) = new_token {
200 let cookie_header =
201 better_auth_core::utils::cookie_utils::create_session_cookie(&token, &ctx.config);
202 Ok(auth_response.with_header("Set-Cookie", cookie_header))
203 } else {
204 Ok(auth_response)
205 }
206 }
207
208 async fn handle_verify_password(
209 &self,
210 req: &AuthRequest,
211 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
212 ) -> AuthResult<AuthResponse> {
213 let body: VerifyPasswordRequest = match better_auth_core::validate_request_body(req) {
214 Ok(v) => v,
215 Err(resp) => return Ok(resp),
216 };
217
218 let user = self.get_current_user(req, ctx).await?;
219 let Some(user) = user else {
220 return Ok(AuthResponse::new(401).with_header("content-type", "application/json"));
221 };
222 let response = verify_password_core(&body, &user, &self.config, ctx).await?;
223 Ok(AuthResponse::json(200, &response)?)
224 }
225
226 async fn handle_reset_password_token(
227 &self,
228 token: &str,
229 req: &AuthRequest,
230 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
231 ) -> AuthResult<AuthResponse> {
232 let query = ResetPasswordTokenQuery {
233 callback_url: req.query.get("callbackURL").cloned(),
234 };
235 match reset_password_token_core(token, &query, ctx).await? {
236 ResetPasswordTokenResult::Redirect(url) => {
237 let mut headers = better_auth_core::Headers::new();
238 let _ = headers.insert("Location".to_string(), url);
239 let _ = headers.insert("content-type".to_string(), "application/json".to_string());
240 Ok(AuthResponse {
241 status: 302,
242 headers,
243 body: Vec::new(),
244 })
245 }
246 }
247 }
248
249 async fn get_current_user<S: better_auth_core::AuthSchema>(
250 &self,
251 req: &AuthRequest,
252 ctx: &AuthContext<S>,
253 ) -> AuthResult<Option<S::User>> {
254 let session_manager = ctx.session_manager();
255
256 if let Some(token) = session_manager.extract_session_token(req)
257 && let Some(session) = session_manager.get_session(&token).await?
258 {
259 return ctx.database.get_user_by_id(&session.user_id()).await;
260 }
261
262 Ok(None)
263 }
264}
265
266#[cfg(test)]
267impl PasswordManagementPlugin {
268 fn validate_password(
269 &self,
270 password: &str,
271 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
272 ) -> AuthResult<()> {
273 better_auth_core::utils::password::validate_password(
274 password,
275 ctx.config.password.min_length,
276 usize::MAX,
277 ctx,
278 )
279 }
280
281 async fn hash_password(&self, password: &str) -> AuthResult<String> {
282 better_auth_core::utils::password::hash_password(
283 self.config.password_hasher.as_ref(),
284 password,
285 )
286 .await
287 }
288
289 async fn verify_password(&self, password: &str, hash: &str) -> AuthResult<()> {
290 better_auth_core::utils::password::verify_password(
291 self.config.password_hasher.as_ref(),
292 password,
293 hash,
294 )
295 .await
296 }
297}