1use std::sync::Arc;
2
3use better_auth_core::utils::username::{
4 UsernameValidationError, normalize_username_fields, validate_username,
5};
6use better_auth_core::{
7 AuthConfig, AuthContext, AuthError, AuthInitContext, AuthPlugin, AuthRequest, AuthResponse,
8 AuthResult, AuthSchema, AuthStore, BeforeRequestAction, EmailProvider,
9 ErrorCodeMessageResponse, HttpMethod, OkResponse, OpenApiBuilder, OpenApiSpec, SessionManager,
10 UpdateUser, UpdateUserRequest, core_paths,
11 entity::{AuthSession, AuthUser},
12 hooks::{RequestHookContext, with_request_hook_context_value},
13 middleware::{
14 self, BodyLimitConfig, BodyLimitMiddleware, CorsConfig, CorsMiddleware, CsrfConfig,
15 CsrfMiddleware, Middleware, RateLimitConfig, RateLimitMiddleware,
16 },
17};
18
19fn username_error_response(status: u16, code: &str, message: &str) -> AuthResult<AuthResponse> {
20 AuthResponse::json(
21 status,
22 &ErrorCodeMessageResponse {
23 code: code.to_string(),
24 message: message.to_string(),
25 },
26 )
27 .map_err(AuthError::from)
28}
29
30pub struct BetterAuth<S: AuthSchema> {
31 config: Arc<AuthConfig>,
32 plugins: Vec<Box<dyn AuthPlugin<S>>>,
33 middlewares: Vec<Box<dyn Middleware>>,
34 body_limit: BodyLimitConfig,
35 store: Arc<dyn AuthStore<S>>,
36 session_manager: SessionManager<S>,
37 context: AuthContext<S>,
38}
39
40pub struct AuthBuilder<S: AuthSchema> {
42 config: AuthConfig,
43 store: Option<Arc<dyn AuthStore<S>>>,
44 plugins: Vec<Box<dyn AuthPlugin<S>>>,
45 csrf_config: Option<CsrfConfig>,
46 rate_limit_config: Option<RateLimitConfig>,
47 cors_config: Option<CorsConfig>,
48 body_limit_config: Option<BodyLimitConfig>,
49 custom_middlewares: Vec<Box<dyn Middleware>>,
50}
51
52impl<S: AuthSchema> AuthBuilder<S> {
53 pub fn new(config: AuthConfig) -> Self {
54 Self {
55 config,
56 store: None,
57 plugins: Vec::new(),
58 csrf_config: None,
59 rate_limit_config: None,
60 cors_config: None,
61 body_limit_config: None,
62 custom_middlewares: Vec::new(),
63 }
64 }
65
66 pub fn store<T>(mut self, store: T) -> Self
68 where
69 T: AuthStore<S> + 'static,
70 {
71 self.store = Some(Arc::new(store));
72 self
73 }
74
75 pub fn store_arc(mut self, store: Arc<dyn AuthStore<S>>) -> Self {
77 self.store = Some(store);
78 self
79 }
80
81 pub fn plugin<P: AuthPlugin<S> + 'static>(mut self, plugin: P) -> Self {
83 self.plugins.push(Box::new(plugin));
84 self
85 }
86
87 pub fn csrf(mut self, config: CsrfConfig) -> Self {
89 self.csrf_config = Some(config);
90 self
91 }
92
93 pub fn rate_limit(mut self, config: RateLimitConfig) -> Self {
95 self.rate_limit_config = Some(config);
96 self
97 }
98
99 pub fn cors(mut self, config: CorsConfig) -> Self {
101 self.cors_config = Some(config);
102 self
103 }
104
105 pub fn body_limit(mut self, config: BodyLimitConfig) -> Self {
107 self.body_limit_config = Some(config);
108 self
109 }
110
111 pub fn email_provider<E: EmailProvider + 'static>(mut self, provider: E) -> Self {
113 self.config.email_provider = Some(Arc::new(provider));
114 self
115 }
116
117 pub fn middleware<M: Middleware + 'static>(mut self, mw: M) -> Self {
119 self.custom_middlewares.push(Box::new(mw));
120 self
121 }
122
123 pub async fn build(self) -> AuthResult<BetterAuth<S>> {
125 self.config.validate()?;
127
128 let config = Arc::new(self.config);
129 let store = self
130 .store
131 .ok_or_else(|| AuthError::config("Auth store not configured"))?;
132
133 let mut init_context = AuthInitContext::new(config.clone(), store.clone());
134
135 for plugin in &self.plugins {
137 plugin.on_init(&mut init_context).await?;
138 }
139
140 let init_parts = init_context.into_parts();
141
142 let session_manager = SessionManager::new(config.clone(), store.clone());
144
145 let context =
147 AuthContext::with_metadata(config.clone(), store.clone(), init_parts.metadata);
148
149 let body_limit = self.body_limit_config.unwrap_or_default();
150
151 let mut middlewares: Vec<Box<dyn Middleware>> = vec![
153 Box::new(BodyLimitMiddleware::new(body_limit.clone())),
154 Box::new(RateLimitMiddleware::new(
155 self.rate_limit_config.unwrap_or_default(),
156 )),
157 Box::new(CsrfMiddleware::new(
158 self.csrf_config.unwrap_or_default(),
159 config.clone(),
160 )),
161 Box::new(CorsMiddleware::new(self.cors_config.unwrap_or_default())),
162 ];
163
164 middlewares.extend(self.custom_middlewares);
165
166 Ok(BetterAuth {
167 config,
168 plugins: self.plugins,
169 middlewares,
170 body_limit,
171 store,
172 session_manager,
173 context,
174 })
175 }
176}
177
178impl<S: AuthSchema> BetterAuth<S> {
179 #[expect(
181 clippy::new_ret_no_self,
182 reason = "returns AuthBuilder by design — builder pattern entry point"
183 )]
184 pub fn new(config: AuthConfig) -> AuthBuilder<S> {
185 AuthBuilder::new(config)
186 }
187}
188
189impl<S: AuthSchema> BetterAuth<S> {
190 pub async fn handle_request(&self, req: AuthRequest) -> AuthResult<AuthResponse> {
196 let mut req =
199 AuthRequest::from_parts(req.method, req.path, req.headers, req.body, req.query);
200
201 let request_context = RequestHookContext::from_request(&req);
202 with_request_hook_context_value(request_context, async {
203 match self.handle_request_inner(&mut req).await {
204 Ok(response) => {
205 middleware::run_after(&self.middlewares, &req, response).await
207 }
208 Err(err) => {
209 let response = err.to_auth_response();
211 middleware::run_after(&self.middlewares, &req, response).await
212 }
213 }
214 })
215 .await
216 }
217
218 async fn handle_request_inner(&self, req: &mut AuthRequest) -> AuthResult<AuthResponse> {
220 if let Some(response) = middleware::run_before(&self.middlewares, req).await? {
222 return Ok(response);
223 }
224
225 let base_path = &self.config.base_path;
231 let stripped_path = if !base_path.is_empty() && base_path != "/" {
232 req.path().strip_prefix(base_path).unwrap_or(req.path())
233 } else {
234 req.path()
235 };
236
237 let mut internal_req = if stripped_path != req.path() {
239 let mut r = req.clone();
240 r.path = stripped_path.to_string();
241 r
242 } else {
243 req.clone()
244 };
245
246 if self.config.is_path_disabled(internal_req.path()) {
248 return Err(AuthError::not_found("This endpoint has been disabled"));
249 }
250
251 for plugin in &self.plugins {
254 if let Some(action) = plugin.before_request(&internal_req, &self.context).await? {
255 match action {
256 BeforeRequestAction::Respond(response) => {
257 return Ok(response);
258 }
259 BeforeRequestAction::InjectSession {
260 user_id,
261 session_token: _,
262 } => {
263 internal_req.set_virtual_user_id(user_id);
269 }
270 }
271 }
272 }
273
274 if let Some(response) = self.handle_core_request(&internal_req).await? {
276 return Ok(response);
277 }
278
279 for plugin in &self.plugins {
281 if let Some(response) = plugin.on_request(&internal_req, &self.context).await? {
282 return Ok(response);
283 }
284 }
285
286 Err(AuthError::not_found("No handler found for this request"))
288 }
289
290 pub fn config(&self) -> &AuthConfig {
292 &self.config
293 }
294
295 pub fn store(&self) -> &Arc<dyn AuthStore<S>> {
297 &self.store
298 }
299
300 pub fn body_limit(&self) -> &BodyLimitConfig {
305 &self.body_limit
306 }
307
308 pub fn session_manager(&self) -> &SessionManager<S> {
310 &self.session_manager
311 }
312
313 pub fn routes(&self) -> Vec<(String, &dyn AuthPlugin<S>)> {
315 let mut routes = Vec::new();
316 for plugin in &self.plugins {
317 for route in plugin.routes() {
318 routes.push((route.path, plugin.as_ref()));
319 }
320 }
321 routes
322 }
323
324 pub fn plugins(&self) -> &[Box<dyn AuthPlugin<S>>] {
326 &self.plugins
327 }
328
329 pub fn get_plugin(&self, name: &str) -> Option<&dyn AuthPlugin<S>> {
331 self.plugins
332 .iter()
333 .find(|p| p.name() == name)
334 .map(|p| p.as_ref())
335 }
336
337 pub fn plugin_names(&self) -> Vec<&'static str> {
339 self.plugins.iter().map(|p| p.name()).collect()
340 }
341
342 pub fn openapi_spec(&self) -> OpenApiSpec {
344 let mut builder = OpenApiBuilder::new("Better Auth", env!("CARGO_PKG_VERSION"))
345 .description("Authentication API")
346 .core_routes();
347
348 for plugin in &self.plugins {
349 builder = builder.plugin(plugin.as_ref());
350 }
351
352 builder.build()
353 }
354
355 async fn handle_core_request(&self, req: &AuthRequest) -> AuthResult<Option<AuthResponse>> {
357 match (req.method(), req.path()) {
358 (HttpMethod::Get, core_paths::OK) => {
359 Ok(Some(AuthResponse::json(200, &OkResponse { ok: true })?))
360 }
361 (HttpMethod::Get, core_paths::ERROR) => {
362 let error_code = req
363 .query
364 .get("error")
365 .cloned()
366 .unwrap_or_else(|| "UNKNOWN".to_string());
367 let error_description = req.query.get("error_description").map(String::as_str);
368 let html = better_auth_core::config::core_paths::error_page_html_with_description(
369 &error_code,
370 error_description,
371 );
372 Ok(Some(AuthResponse::html(200, html)))
373 }
374 (HttpMethod::Get, core_paths::OPENAPI_SPEC) => {
375 let spec = self.openapi_spec();
376 Ok(Some(AuthResponse::json(200, &spec)?))
377 }
378 (HttpMethod::Post, core_paths::UPDATE_USER) => {
379 Ok(Some(self.handle_update_user(req).await?))
380 }
381 _ => Ok(None),
382 }
383 }
384
385 async fn handle_update_user(&self, req: &AuthRequest) -> AuthResult<AuthResponse> {
387 let current_user = self.extract_current_user(req).await?;
388 let body: serde_json::Value = req
389 .body_as_json()
390 .map_err(|e| AuthError::bad_request(format!("Invalid JSON: {}", e)))?;
391 let body = match body.as_object() {
392 Some(body) => body,
393 None => {
394 let actual = match &body {
395 serde_json::Value::Null => "null",
396 serde_json::Value::Bool(_) => "boolean",
397 serde_json::Value::Number(_) => "number",
398 serde_json::Value::String(_) => "string",
399 serde_json::Value::Array(_) => "array",
400 serde_json::Value::Object(_) => "record",
401 };
402 return Ok(AuthResponse::json(
403 400,
404 &better_auth_core::ErrorCodeMessageResponse {
405 code: "VALIDATION_ERROR".to_string(),
406 message: format!(
407 "[body] Invalid input: expected record, received {}",
408 actual
409 ),
410 },
411 )?);
412 }
413 };
414
415 if body.contains_key("email") {
416 return Err(AuthError::bad_request("Email can not be updated"));
417 }
418
419 let update_req: UpdateUserRequest =
420 serde_json::from_value(serde_json::Value::Object(body.clone()))
421 .map_err(|e| AuthError::bad_request(format!("Invalid JSON: {}", e)))?;
422 let (username, display_username) =
423 normalize_username_fields(update_req.username, update_req.display_username);
424
425 if let Some(username) = username.as_deref() {
426 match validate_username(username) {
427 Ok(()) => {}
428 Err(UsernameValidationError::TooShort) => {
429 return username_error_response(
430 400,
431 "USERNAME_TOO_SHORT",
432 "Username is too short",
433 );
434 }
435 Err(UsernameValidationError::TooLong) => {
436 return username_error_response(
437 400,
438 "USERNAME_IS_TOO_LONG",
439 "Username is too long",
440 );
441 }
442 Err(UsernameValidationError::Invalid) => {
443 return username_error_response(
444 400,
445 "USERNAME_IS_INVALID",
446 "Username is invalid",
447 );
448 }
449 }
450
451 if let Some(existing_user) = self.store.get_user_by_username(username).await?
452 && existing_user.id() != current_user.id()
453 {
454 return username_error_response(
455 400,
456 "USERNAME_IS_ALREADY_TAKEN",
457 "Username is already taken. Please try another.",
458 );
459 }
460 }
461
462 let has_changes = update_req.name.is_some()
463 || update_req.image.is_some()
464 || username.is_some()
465 || display_username.is_some()
466 || update_req.role.is_some()
467 || update_req.metadata.is_some();
468 if !has_changes {
469 return Err(AuthError::bad_request("No fields to update"));
470 }
471
472 let update_user = UpdateUser {
473 email: None,
474 name: update_req.name,
475 image: update_req.image,
476 email_verified: None,
477 username,
478 display_username,
479 role: update_req.role,
480 banned: None,
481 ban_reason: None,
482 ban_expires: None,
483 two_factor_enabled: None,
484 metadata: update_req.metadata,
485 };
486
487 _ = self
488 .store
489 .update_user(¤t_user.id(), update_user)
490 .await?;
491
492 let mut response =
493 AuthResponse::json(200, &better_auth_core::StatusResponse { status: true })?;
494
495 if let Some(token) = self.session_manager.extract_session_token(req) {
496 let cookie_header =
497 better_auth_core::utils::cookie_utils::create_session_cookie(&token, &self.config);
498 response = response.with_header("Set-Cookie", cookie_header);
499 }
500
501 Ok(response)
502 }
503
504 async fn extract_current_user(&self, req: &AuthRequest) -> AuthResult<S::User> {
511 if let Some(uid) = req.virtual_user_id() {
513 let user = self.store.get_user_by_id(uid).await?;
514 return user.ok_or(AuthError::UserNotFound);
515 }
516
517 let token = self
518 .session_manager
519 .extract_session_token(req)
520 .ok_or(AuthError::Unauthenticated)?;
521
522 let session = self
523 .session_manager
524 .get_session(&token)
525 .await?
526 .ok_or(AuthError::SessionNotFound)?;
527
528 let user = self.store.get_user_by_id(&session.user_id()).await?;
529
530 user.ok_or(AuthError::UserNotFound)
531 }
532}