Skip to main content

better_auth/core/
auth.rs

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
40/// Initial builder for configuring BetterAuth.
41pub 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    /// Set the shared auth store implementation.
67    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    /// Set the shared auth store implementation using an existing [`Arc`].
76    pub fn store_arc(mut self, store: Arc<dyn AuthStore<S>>) -> Self {
77        self.store = Some(store);
78        self
79    }
80
81    /// Add a plugin to the authentication system.
82    pub fn plugin<P: AuthPlugin<S> + 'static>(mut self, plugin: P) -> Self {
83        self.plugins.push(Box::new(plugin));
84        self
85    }
86
87    /// Configure CSRF protection.
88    pub fn csrf(mut self, config: CsrfConfig) -> Self {
89        self.csrf_config = Some(config);
90        self
91    }
92
93    /// Configure rate limiting.
94    pub fn rate_limit(mut self, config: RateLimitConfig) -> Self {
95        self.rate_limit_config = Some(config);
96        self
97    }
98
99    /// Configure CORS.
100    pub fn cors(mut self, config: CorsConfig) -> Self {
101        self.cors_config = Some(config);
102        self
103    }
104
105    /// Configure body size limit.
106    pub fn body_limit(mut self, config: BodyLimitConfig) -> Self {
107        self.body_limit_config = Some(config);
108        self
109    }
110
111    /// Set the email provider.
112    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    /// Add a custom middleware.
118    pub fn middleware<M: Middleware + 'static>(mut self, mw: M) -> Self {
119        self.custom_middlewares.push(Box::new(mw));
120        self
121    }
122
123    /// Build the BetterAuth instance.
124    pub async fn build(self) -> AuthResult<BetterAuth<S>> {
125        // Validate configuration
126        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        // Initialize all plugins.
136        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        // Create session manager
143        let session_manager = SessionManager::new(config.clone(), store.clone());
144
145        // Create context
146        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        // Build middleware chain (order matters: body limit → rate limit → CSRF → CORS → custom)
152        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    /// Create a new BetterAuth builder.
180    #[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    /// Handle an authentication request.
191    ///
192    /// Errors from plugins and core handlers are automatically converted
193    /// into standardized JSON responses via [`AuthError::to_auth_response`],
194    /// producing `{ "message": "..." }` with the appropriate HTTP status code.
195    pub async fn handle_request(&self, req: AuthRequest) -> AuthResult<AuthResponse> {
196        // Ignore any caller-supplied virtual session value; only internal
197        // before_request hooks may inject this during dispatch.
198        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                    // Run after-request middleware chain
206                    middleware::run_after(&self.middlewares, &req, response).await
207                }
208                Err(err) => {
209                    // Convert error to standardized response, then run after-middleware
210                    let response = err.to_auth_response();
211                    middleware::run_after(&self.middlewares, &req, response).await
212                }
213            }
214        })
215        .await
216    }
217
218    /// Inner request handler that may return errors.
219    async fn handle_request_inner(&self, req: &mut AuthRequest) -> AuthResult<AuthResponse> {
220        // Run before-request middleware chain
221        if let Some(response) = middleware::run_before(&self.middlewares, req).await? {
222            return Ok(response);
223        }
224
225        // Strip base_path prefix from the request path for internal routing.
226        // This happens BEFORE plugin hooks so that `before_request` sees the
227        // same normalised path that `on_request` / core handlers use.
228        // External callers send e.g. "/api/auth/sign-in/email"; internally
229        // handlers match against "/sign-in/email".
230        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        // Build a request with the stripped path for all subsequent dispatch
238        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        // Check if this path is disabled
247        if self.config.is_path_disabled(internal_req.path()) {
248            return Err(AuthError::not_found("This endpoint has been disabled"));
249        }
250
251        // Run plugin before_request hooks (e.g. API-key → session emulation)
252        // Plugins now see the normalised (base_path-stripped) path.
253        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                        // Set the virtual user id on the request so that
264                        // `extract_current_user` can resolve the user without
265                        // creating a real database session.  This mirrors the
266                        // TypeScript `ctx.context.session` virtual-session
267                        // approach — no DB writes on every API-key request.
268                        internal_req.set_virtual_user_id(user_id);
269                    }
270                }
271            }
272        }
273
274        // Handle core endpoints first
275        if let Some(response) = self.handle_core_request(&internal_req).await? {
276            return Ok(response);
277        }
278
279        // Try each plugin until one handles the request
280        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        // No handler found
287        Err(AuthError::not_found("No handler found for this request"))
288    }
289
290    /// Get the configuration.
291    pub fn config(&self) -> &AuthConfig {
292        &self.config
293    }
294
295    /// Get the shared auth store used by Better Auth.
296    pub fn store(&self) -> &Arc<dyn AuthStore<S>> {
297        &self.store
298    }
299
300    /// Get the effective request body size limit.
301    ///
302    /// Transports read the body before any middleware runs, so they need this
303    /// to bound the read itself rather than rejecting after buffering.
304    pub fn body_limit(&self) -> &BodyLimitConfig {
305        &self.body_limit
306    }
307
308    /// Get the session manager.
309    pub fn session_manager(&self) -> &SessionManager<S> {
310        &self.session_manager
311    }
312
313    /// Get all routes from plugins.
314    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    /// Get all plugins.
325    pub fn plugins(&self) -> &[Box<dyn AuthPlugin<S>>] {
326        &self.plugins
327    }
328
329    /// Get plugin by name.
330    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    /// List all plugin names.
338    pub fn plugin_names(&self) -> Vec<&'static str> {
339        self.plugins.iter().map(|p| p.name()).collect()
340    }
341
342    /// Generate the OpenAPI spec for all registered routes.
343    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    /// Handle core authentication requests.
356    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    /// Handle user profile update.
386    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(&current_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    /// Extract current user from request (validates session).
505    ///
506    /// If a virtual session was injected by a `before_request` hook (e.g.
507    /// API-key session emulation), the user is resolved directly by ID
508    /// **without** a database session lookup — matching the TypeScript
509    /// `ctx.context.session` virtual-session behaviour.
510    async fn extract_current_user(&self, req: &AuthRequest) -> AuthResult<S::User> {
511        // Fast path: virtual session injected by before_request hook
512        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}