Skip to main content

better_auth_core/
plugin.rs

1use async_trait::async_trait;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use crate::config::AuthConfig;
6use crate::email::EmailProvider;
7use crate::error::{AuthError, AuthResult};
8use crate::schema::AuthSchema;
9#[cfg(test)]
10use crate::session::SessionManager;
11use crate::store::AuthStore;
12use crate::types::{AuthRequest, AuthResponse, HttpMethod};
13
14/// Runtime metadata published by enabled plugins.
15pub type MetadataMap = HashMap<String, serde_json::Value>;
16
17pub struct AuthInitParts<S: AuthSchema> {
18    request_runtime: crate::request_runtime::RequestRuntime,
19    pub plugin_fields: crate::plugin_runtime::ModelFields,
20    pub database_hooks: Vec<Arc<dyn crate::store::database_hooks::DatabaseHooks<S>>>,
21    pub runtime: crate::plugin_runtime::PluginRuntime<S>,
22    pub extensions: crate::RuntimeExtensions,
23    pub email_verification_policy: crate::email::EmailVerificationRuntimePolicy,
24    pub metadata: MetadataMap,
25    pub email_provider: Option<Arc<dyn EmailProvider>>,
26    pub secondary_storage: Option<Arc<dyn crate::store::SecondaryStorage>>,
27    pub password_policy: crate::utils::password::PasswordRuntimePolicy,
28}
29
30/// Action returned by [`AuthPlugin::before_request`].
31#[derive(Debug)]
32pub enum BeforeRequestAction {
33    /// Short-circuit with this response (e.g. return session JSON).
34    Respond(AuthResponse),
35    /// Replace the request body and continue with the remaining hooks.
36    ReplaceBody(Vec<u8>),
37    /// Merge endpoint body/query after all before hooks; retain original Request bytes.
38    MergeContext(crate::endpoint_input::EndpointInputPatch),
39    /// Inject a virtual session so downstream handlers see it as authenticated.
40    InjectSession {
41        session: Box<crate::wire::SessionView>,
42    },
43}
44
45/// Plugin trait that all authentication plugins must implement.
46///
47#[async_trait]
48pub trait AuthPlugin<S: AuthSchema>: Send + Sync + std::any::Any {
49    /// Plugin name - should be unique
50    fn name(&self) -> &'static str;
51
52    /// Return the upstream plugin ID; core option wrappers have no plugin ID.
53    fn telemetry_plugin_id(&self) -> Option<&'static str> {
54        Some(self.name())
55    }
56
57    /// Routes that this plugin handles
58    fn routes(&self) -> Vec<AuthRoute>;
59
60    /// Document this configured plugin without invoking application field policies.
61    fn openapi(&self) -> AuthResult<crate::openapi::OpenApiPluginMetadata> {
62        crate::openapi::OpenApiPluginMetadata::from_routes(self.name(), self.routes())
63    }
64
65    /// Select the base password hasher before plugin initialization wraps it.
66    fn password_hasher(&self) -> Option<Arc<dyn crate::utils::password::PasswordHasher>> {
67        None
68    }
69
70    /// Report configured options without invoking callbacks or initializing the plugin.
71    fn telemetry(&self, _options: &mut crate::observability::telemetry::PluginTelemetry) {}
72
73    /// Default endpoint limits, overridden by explicit application limits.
74    fn rate_limits(&self) -> AuthResult<Vec<crate::middleware::PluginRateLimit>> {
75        Ok(Vec::new())
76    }
77
78    /// Called when the plugin is initialized
79    async fn on_init(&self, ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
80        let _ = ctx;
81        Ok(())
82    }
83
84    /// Inspect an HTTP request after rate limiting and before endpoint middleware.
85    /// Native API calls do not run this hook. A response skips endpoint hooks.
86    async fn on_http_request(
87        &self,
88        _req: &AuthRequest,
89        _ctx: &AuthContext<S>,
90    ) -> AuthResult<Option<AuthResponse>> {
91        Ok(None)
92    }
93
94    /// Inspect the HTTP router response, including route misses and body errors.
95    /// Early HTTP request responses and native endpoint calls skip this hook.
96    /// A replacement response stops the remaining HTTP response hooks.
97    async fn on_http_response(
98        &self,
99        _req: &AuthRequest,
100        _response: &mut AuthResponse,
101        _ctx: &AuthContext<S>,
102    ) -> AuthResult<Option<AuthResponse>> {
103        Ok(None)
104    }
105
106    /// Called for a registered endpoint after HTTP body decoding and origin checks.
107    ///
108    /// Return `Some(BeforeRequestAction::Respond(..))` to short-circuit with a
109    /// response, `Some(BeforeRequestAction::InjectSession { .. })` to attach a
110    /// virtual session, or `Some(BeforeRequestAction::ReplaceBody(..))` to rewrite
111    /// the request body. Session injection and body replacement continue through
112    /// the remaining hooks before normal route matching.
113    async fn before_request(
114        &self,
115        _req: &AuthRequest,
116        _ctx: &AuthContext<S>,
117    ) -> AuthResult<Option<BeforeRequestAction>> {
118        Ok(None)
119    }
120
121    /// Called for each request - return Some(response) to handle, None to pass through
122    async fn on_request(
123        &self,
124        req: &AuthRequest,
125        ctx: &AuthContext<S>,
126    ) -> AuthResult<Option<AuthResponse>>;
127
128    /// Inspect or extend the final route response after session cookies are applied.
129    async fn after_request(
130        &self,
131        _req: &AuthRequest,
132        _response: &mut AuthResponse,
133        _ctx: &AuthContext<S>,
134    ) -> AuthResult<()> {
135        Ok(())
136    }
137}
138
139/// Generates the [`AuthPlugin`] impl for a plugin with static route dispatch.
140///
141/// Eliminates the dual declaration of routes in `routes()` and `on_request()`
142/// by generating both from a single route table.
143///
144/// # Exceptions (must keep manual impl)
145/// - `OAuthPlugin` — dynamic path matching for `/callback/{provider}`
146/// - `SessionManagementPlugin` — match guards and OR patterns
147/// - `EmailPasswordPlugin` — conditional routes based on config
148/// - `UserManagementPlugin` — conditional routes based on config
149/// - `PasswordManagementPlugin` — dynamic path matching for `/reset-password/{token}`
150/// - `OrganizationPlugin` — handlers accept extra `&self.config` argument
151#[macro_export]
152macro_rules! impl_auth_plugin {
153    (@pat get) => { $crate::HttpMethod::Get };
154    (@pat post) => { $crate::HttpMethod::Post };
155    (@pat put) => { $crate::HttpMethod::Put };
156    (@pat delete) => { $crate::HttpMethod::Delete };
157    (@pat patch) => { $crate::HttpMethod::Patch };
158    (@pat head) => { $crate::HttpMethod::Head };
159
160    (@route get) => { $crate::AuthRoute::get };
161    (@route post) => { $crate::AuthRoute::post };
162    (@route put) => { $crate::AuthRoute::put };
163    (@route delete) => { $crate::AuthRoute::delete };
164
165    (
166        $plugin:ty, $name:expr;
167        routes {
168            $( $method:ident $path:literal => $handler:ident, $op_id:literal
169                $(, allowed_media_types = [$($media_type:literal),* $(,)?])?
170                $(, body = $body:path)?
171                $(, query = $query:path)?
172                $(, require_headers = $require_headers:literal)?
173            );* $(;)?
174        }
175        $( extra { $($extra:tt)* } )?
176    ) => {
177        #[::async_trait::async_trait]
178        impl<S: $crate::AuthSchema> $crate::AuthPlugin<S> for $plugin {
179            fn name(&self) -> &'static str { $name }
180
181            fn routes(&self) -> Vec<$crate::AuthRoute> {
182                vec![
183                    $( $crate::AuthRoute::new($crate::impl_auth_plugin!(@pat $method), $path, $op_id)
184                        $(.allowed_media_types(&[$($media_type),*]))?
185                        $(.body_validator($body))?
186                        $(.query_validator($query))?
187                        $(.require_headers($require_headers))?, )*
188                ]
189            }
190
191            async fn on_request(
192                &self,
193                req: &$crate::AuthRequest,
194                ctx: &$crate::AuthContext<S>,
195            ) -> $crate::AuthResult<Option<$crate::AuthResponse>> {
196                match (req.method(), req.path()) {
197                    $(
198                        ($crate::impl_auth_plugin!(@pat $method), $path) => {
199                            Ok(Some(self.$handler(req, ctx).await?))
200                        }
201                    )*
202                    _ => Ok(None),
203                }
204            }
205
206            $( $($extra)* )?
207        }
208    };
209}
210
211/// Validate a raw endpoint query without changing the original request.
212pub type QueryValidator = fn(Option<serde_json::Value>) -> AuthResult<Option<serde_json::Value>>;
213
214pub use crate::endpoint_input::BodyValidator;
215
216/// Route definition for plugins
217#[derive(Clone)]
218pub struct AuthRoute {
219    /// Native-only endpoints cannot match HTTP routing.
220    pub server_only: bool,
221    pub path: String,
222    pub method: HttpMethod,
223    /// Identifier used as the OpenAPI `operationId` for this route.
224    pub operation_id: String,
225    /// Explicit OpenAPI documentation. This does not validate endpoint input.
226    pub openapi: Option<crate::openapi::OpenApiRouteMetadata>,
227    /// Upstream API key used when excluding plugin endpoints that replace core endpoints.
228    pub endpoint_key: Option<String>,
229    /// HTTP media types accepted before endpoint middleware. Empty uses the JSON default.
230    pub allowed_media_types: Vec<String>,
231    /// Query validation runs after before hooks and before endpoint middleware.
232    pub query_validator: Option<QueryValidator>,
233    pub body_validator: Option<BodyValidator>,
234    /// Header presence is validated after body and query, before endpoint middleware.
235    pub require_headers: bool,
236}
237
238/// Initialization context passed to plugin setup.
239pub struct AuthInitContext<S: AuthSchema> {
240    request_runtime: crate::request_runtime::RequestRuntime,
241    plugin_fields: crate::plugin_runtime::ModelFields,
242    registered_model_field_names: Vec<(crate::store::schema::EntityRole, Vec<String>)>,
243    database_hooks: Vec<Arc<dyn crate::store::database_hooks::DatabaseHooks<S>>>,
244    runtime: crate::plugin_runtime::PluginRuntime<S>,
245    pub extensions: crate::RuntimeExtensions,
246    pub email_verification_policy: crate::email::EmailVerificationRuntimePolicy,
247    pub config: Arc<AuthConfig>,
248    pub database: Arc<dyn AuthStore<S>>,
249    pub email_provider: Option<Arc<dyn EmailProvider>>,
250    pub secondary_storage: Option<Arc<dyn crate::store::SecondaryStorage>>,
251    pub password_policy: crate::utils::password::PasswordRuntimePolicy,
252    pub metadata: MetadataMap,
253}
254
255/// Context passed to plugin methods.
256pub struct AuthContext<S: AuthSchema> {
257    pub(crate) request_runtime: crate::request_runtime::RequestRuntime,
258    pub extensions: crate::RuntimeExtensions,
259    pub email_verification_policy: crate::email::EmailVerificationRuntimePolicy,
260    pub config: Arc<AuthConfig>,
261    pub database: Arc<dyn AuthStore<S>>,
262    pub email_provider: Option<Arc<dyn EmailProvider>>,
263    pub secondary_storage: Option<Arc<dyn crate::store::SecondaryStorage>>,
264    pub password_policy: crate::utils::password::PasswordRuntimePolicy,
265    pub metadata: MetadataMap,
266}
267
268impl<S: AuthSchema> Clone for AuthContext<S> {
269    fn clone(&self) -> Self {
270        Self {
271            request_runtime: self.request_runtime.clone(),
272            extensions: self.extensions.clone(),
273            email_verification_policy: self.email_verification_policy.clone(),
274            config: self.config.clone(),
275            database: self.database.clone(),
276            email_provider: self.email_provider.clone(),
277            secondary_storage: self.secondary_storage.clone(),
278            password_policy: self.password_policy.clone(),
279            metadata: self.metadata.clone(),
280        }
281    }
282}
283
284impl std::fmt::Debug for AuthRoute {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        f.debug_struct("AuthRoute")
287            .field("path", &self.path)
288            .field("method", &self.method)
289            .field("operation_id", &self.operation_id)
290            .field("openapi", &self.openapi)
291            .field("endpoint_key", &self.endpoint_key)
292            .field("allowed_media_types", &self.allowed_media_types)
293            .field("query_validator", &self.query_validator)
294            .field("require_headers", &self.require_headers)
295            .field(
296                "body_validator",
297                &self.body_validator.as_ref().map(|_| "configured"),
298            )
299            .finish()
300    }
301}
302
303impl AuthRoute {
304    /// Define an endpoint that is available only through trusted native facades.
305    pub fn server_only(method: HttpMethod, operation_id: impl Into<String>) -> Self {
306        let mut route = Self::new(method, "/", operation_id);
307        route.server_only = true;
308        route
309    }
310
311    /// Match the method and slash-separated path, including named `{parameter}` segments.
312    pub fn matches(&self, method: &HttpMethod, path: &str) -> bool {
313        if self.server_only || self.method != *method {
314            return false;
315        }
316        let mut actual = path.split('/');
317        for expected in self.path.split('/') {
318            let Some(actual) = actual.next() else {
319                return false;
320            };
321            if expected != actual
322                && !(expected.starts_with('{') && expected.ends_with('}') && !actual.is_empty())
323            {
324                return false;
325            }
326        }
327        actual.next().is_none()
328    }
329
330    pub fn openapi(mut self, metadata: crate::openapi::OpenApiRouteMetadata) -> Self {
331        self.openapi = Some(metadata);
332        self
333    }
334
335    pub fn endpoint_key(mut self, key: impl Into<String>) -> Self {
336        self.endpoint_key = Some(key.into());
337        self
338    }
339
340    /// Install a body validator. Body validation precedes query validation.
341    pub fn body_validator(
342        mut self,
343        validator: impl Fn(&AuthRequest) -> AuthResult<crate::endpoint_input::ValidatedBody>
344        + Send
345        + Sync
346        + 'static,
347    ) -> Self {
348        self.body_validator = Some(BodyValidator::new(validator));
349        self
350    }
351
352    /// Install an asynchronous body validator at the same phase as synchronous validation.
353    pub fn body_validator_async<F, Fut>(mut self, validator: F) -> Self
354    where
355        F: Fn(AuthRequest) -> Fut + Send + Sync + 'static,
356        Fut: std::future::Future<Output = AuthResult<crate::endpoint_input::ValidatedBody>>
357            + Send
358            + 'static,
359    {
360        self.body_validator = Some(BodyValidator::new_async(validator));
361        self
362    }
363
364    /// Install a query validator for the endpoint handler and its middleware.
365    pub fn query_validator(mut self, validator: QueryValidator) -> Self {
366        self.query_validator = Some(validator);
367        self
368    }
369
370    pub fn require_headers(mut self, required: bool) -> Self {
371        self.require_headers = required;
372        self
373    }
374
375    pub fn allowed_media_types(mut self, types: &[&str]) -> Self {
376        self.allowed_media_types = types.iter().map(|value| (*value).to_owned()).collect();
377        self
378    }
379
380    pub fn new(
381        method: HttpMethod,
382        path: impl Into<String>,
383        operation_id: impl Into<String>,
384    ) -> Self {
385        Self {
386            server_only: false,
387            path: path.into(),
388            method,
389            operation_id: operation_id.into(),
390            openapi: None,
391            endpoint_key: None,
392            allowed_media_types: Vec::new(),
393            query_validator: None,
394            body_validator: None,
395            require_headers: false,
396        }
397    }
398
399    pub fn get(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
400        Self::new(HttpMethod::Get, path, operation_id)
401    }
402
403    pub fn post(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
404        Self::new(HttpMethod::Post, path, operation_id)
405    }
406
407    pub fn put(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
408        Self::new(HttpMethod::Put, path, operation_id)
409    }
410
411    pub fn delete(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
412        Self::new(HttpMethod::Delete, path, operation_id)
413    }
414}
415
416impl<S: AuthSchema> AuthInitContext<S> {
417    pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
418        let email_provider = config.email_provider.clone();
419        let password_policy = crate::utils::password::PasswordRuntimePolicy::new(&config.password);
420        Self {
421            request_runtime: Default::default(),
422            plugin_fields: Default::default(),
423            registered_model_field_names: Vec::new(),
424            database_hooks: Vec::new(),
425            runtime: Default::default(),
426            extensions: crate::RuntimeExtensions::default(),
427            email_verification_policy: crate::email::EmailVerificationRuntimePolicy::default(),
428            config,
429            database,
430            email_provider,
431            secondary_storage: None,
432            password_policy,
433            metadata: MetadataMap::new(),
434        }
435    }
436
437    /// Resolve initial URL and trust options before plugin initialization.
438    pub async fn initialize_request_context(&mut self) -> AuthResult<()> {
439        let mut context = AuthContext::new(self.config.clone(), self.database.clone());
440        context.request_runtime = self.request_runtime.clone();
441        let context = context.initialize_request_context().await?;
442        self.config = context.config.clone();
443        self.request_runtime = context.request_runtime.clone();
444        Ok(())
445    }
446
447    /// Register user schema fields in plugin registration order.
448    pub fn register_user_fields(&mut self, fields: crate::user_fields::UserConfig) {
449        let names = fields
450            .additional_fields
451            .as_ref()
452            .map(|fields| fields.keys().cloned().collect());
453        self.plugin_fields
454            .extend(crate::store::schema::EntityRole::User, fields);
455        if let Some(names) = names {
456            self.registered_model_field_names
457                .push((crate::store::schema::EntityRole::User, names));
458        }
459    }
460
461    /// Register adapter fields for core models, Organization, Team, or supported plugin display fields.
462    /// Other plugin roles and fields return a configuration error.
463    pub fn register_model_fields(
464        &mut self,
465        role: crate::store::schema::EntityRole,
466        fields: crate::user_fields::UserConfig,
467    ) -> AuthResult<()> {
468        let names = fields
469            .additional_fields
470            .as_ref()
471            .map(|fields| fields.keys().cloned().collect());
472        self.plugin_fields.register(role, fields)?;
473        if let Some(names) = names {
474            self.registered_model_field_names.push((role, names));
475        }
476        Ok(())
477    }
478
479    /// Declare a custom model for runtime reference resolution and adapter schema metadata.
480    /// Repeated declarations merge field policies and replace the physical model name.
481    /// Omitted or empty physical names use the logical name. Native logical names are reserved.
482    /// The application's storage adapter must provide custom model operations and migrations.
483    pub fn register_custom_model(
484        &mut self,
485        name: impl Into<String>,
486        model_name: Option<&str>,
487        fields: crate::user_fields::UserConfig,
488    ) -> AuthResult<()> {
489        self.plugin_fields
490            .register_custom_model(name.into(), model_name, fields)
491    }
492
493    /// Take successful declarations after one plugin initializes, before the next plugin runs.
494    /// Each role group retains explicit empty declarations and its field insertion order.
495    #[doc(hidden)]
496    pub fn take_registered_model_field_names(
497        &mut self,
498    ) -> Vec<(crate::store::schema::EntityRole, Vec<String>)> {
499        std::mem::take(&mut self.registered_model_field_names)
500    }
501
502    /// Register the Organization plugin schema at its position in plugin initialization.
503    #[doc(hidden)]
504    pub fn register_organization_schema(
505        &mut self,
506        fields: &crate::organization_fields::OrganizationFields,
507        teams_enabled: bool,
508    ) {
509        self.plugin_fields
510            .register_organization_schema(fields, teams_enabled);
511    }
512
513    /// Register a database hook before application-owned adapter hooks.
514    pub fn register_database_hook(
515        &mut self,
516        hook: Arc<dyn crate::store::database_hooks::DatabaseHooks<S>>,
517    ) {
518        self.database_hooks.push(hook);
519    }
520
521    /// Obtain a weak handle that resolves after all store facades are installed.
522    pub fn runtime(&self) -> crate::plugin_runtime::PluginRuntime<S> {
523        self.runtime.clone()
524    }
525
526    pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
527        _ = self.metadata.insert(key.into(), value);
528    }
529
530    pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
531        self.metadata.get(key)
532    }
533
534    pub fn into_parts(self) -> AuthInitParts<S> {
535        AuthInitParts {
536            request_runtime: self.request_runtime,
537            plugin_fields: self.plugin_fields,
538            database_hooks: self.database_hooks,
539            runtime: self.runtime,
540            extensions: self.extensions,
541            email_verification_policy: self.email_verification_policy,
542            metadata: self.metadata,
543            email_provider: self.email_provider,
544            secondary_storage: self.secondary_storage,
545            password_policy: self.password_policy,
546        }
547    }
548}
549
550impl<S: AuthSchema> AuthContext<S> {
551    pub fn new(config: Arc<AuthConfig>, database: Arc<dyn AuthStore<S>>) -> Self {
552        let email_provider = config.email_provider.clone();
553        let password_policy = crate::utils::password::PasswordRuntimePolicy::new(&config.password);
554        Self {
555            request_runtime: Default::default(),
556            extensions: crate::RuntimeExtensions::default(),
557            email_verification_policy: crate::email::EmailVerificationRuntimePolicy::default(),
558            config,
559            database,
560            email_provider,
561            secondary_storage: None,
562            password_policy,
563            metadata: MetadataMap::new(),
564        }
565    }
566
567    pub fn with_metadata(
568        config: Arc<AuthConfig>,
569        database: Arc<dyn AuthStore<S>>,
570        metadata: MetadataMap,
571    ) -> Self {
572        let email_provider = config.email_provider.clone();
573        let password_policy = crate::utils::password::PasswordRuntimePolicy::new(&config.password);
574        Self {
575            request_runtime: Default::default(),
576            extensions: crate::RuntimeExtensions::default(),
577            email_verification_policy: crate::email::EmailVerificationRuntimePolicy::default(),
578            config,
579            database,
580            email_provider,
581            secondary_storage: None,
582            password_policy,
583            metadata,
584        }
585    }
586
587    pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
588        _ = self.metadata.insert(key.into(), value);
589    }
590
591    pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
592        self.metadata.get(key)
593    }
594
595    /// Get the email provider, returning an error if none is configured.
596    pub fn email_provider(&self) -> AuthResult<&dyn EmailProvider> {
597        self.email_provider
598            .as_deref()
599            .ok_or_else(|| AuthError::config("No email provider configured"))
600    }
601
602    /// Explicit application storage configuration; adapter wrappers do not establish authority.
603    pub fn store_capabilities(&self) -> crate::store::StoreCapabilities {
604        self.extensions
605            .get::<crate::store::StoreCapabilities>()
606            .copied()
607            .unwrap_or_default()
608    }
609
610    /// Create a `SessionManager` from this context's config and database.
611    pub fn session_manager(&self) -> crate::session::SessionManager<S> {
612        crate::session::SessionManager::new(self.config.clone(), self.database.clone())
613            .with_store_capabilities(self.store_capabilities())
614            .with_user_metadata(self.metadata.clone())
615            .with_adapter_user_fields(self.adapter_user_fields().clone())
616            .with_cookie_signer(
617                self.extensions
618                    .get::<Arc<dyn crate::session::SessionCookieSigner<S>>>()
619                    .cloned(),
620            )
621    }
622
623    /// User policy used by the adapter before public output filtering.
624    pub fn adapter_user_fields(&self) -> &crate::user_fields::UserConfig {
625        self.extensions
626            .get::<crate::plugin_runtime::AdapterUserFields>()
627            .map_or(&self.config.user, |fields| &fields.0)
628    }
629
630    /// Project a user through the active user schema before returning public data.
631    pub async fn user_view(
632        &self,
633        user: &impl crate::entity::AuthUser,
634    ) -> AuthResult<crate::wire::UserView> {
635        crate::wire::UserView::with_field_policies(
636            user,
637            self.adapter_user_fields(),
638            &self.config.user,
639            &self.metadata,
640            self.database.supports_native_json(),
641        )
642        .await
643    }
644
645    /// Keep hidden user fields available to trusted callbacks.
646    pub async fn internal_user_view(
647        &self,
648        user: &impl crate::entity::AuthUser,
649    ) -> AuthResult<crate::wire::UserView> {
650        crate::wire::UserView::with_internal_fields_for_adapter(
651            user,
652            self.adapter_user_fields(),
653            &self.metadata,
654            self.database.supports_native_json(),
655        )
656        .await
657    }
658
659    /// Preserve cached session fields without repeating database output transforms.
660    pub async fn session_view(
661        &self,
662        session: &impl crate::entity::AuthSession,
663    ) -> AuthResult<crate::wire::SessionView> {
664        self.session_manager().session_view(session).await
665    }
666
667    /// Validate public user fields, including proof and privilege fields owned by active plugins.
668    pub fn parse_user_input(
669        &self,
670        input: &serde_json::Map<String, serde_json::Value>,
671        create: bool,
672    ) -> AuthResult<crate::FieldMap> {
673        let input = crate::FieldMap::from_json(input.clone())?;
674        for (plugin, fields, has_default) in [
675            (
676                "admin.enabled",
677                &["role", "banReason", "banExpires"][..],
678                false,
679            ),
680            ("admin.enabled", &["banned"][..], true),
681            ("phone-number.enabled", &["phoneNumberVerified"][..], false),
682            ("anonymous.enabled", &["isAnonymous"][..], true),
683            ("two_factor.enabled", &["twoFactorEnabled"][..], true),
684        ] {
685            if self
686                .get_metadata(plugin)
687                .and_then(serde_json::Value::as_bool)
688                != Some(true)
689                || (create && has_default)
690            {
691                continue;
692            }
693            for name in fields {
694                if input.get(*name).is_some_and(crate::FieldValue::is_truthy) {
695                    return Err(AuthError::FieldInput {
696                        code: "FIELD_NOT_ALLOWED",
697                        message: format!("{name} is not allowed to be set"),
698                    });
699                }
700            }
701        }
702        self.config.user.parse_input(&input, create)
703    }
704
705    /// Extract a session token from the request, validate the session, and
706    /// return the authenticated `(User, Session)` pair.
707    ///
708    /// This centralises the pattern previously duplicated across many plugins
709    /// (`get_authenticated_user`, `require_session`, etc.).
710    pub async fn require_session(
711        &self,
712        req: &AuthRequest,
713    ) -> AuthResult<(crate::wire::UserView, crate::wire::SessionView)> {
714        self.require_session_with_read(req, crate::session::SessionRead::Cached)
715            .await
716    }
717
718    /// Require a session read from the server store for sensitive work.
719    pub async fn require_authoritative_session(
720        &self,
721        req: &AuthRequest,
722    ) -> AuthResult<(crate::wire::UserView, crate::wire::SessionView)> {
723        self.require_session_with_read(req, crate::session::SessionRead::Authoritative)
724            .await
725    }
726
727    async fn require_session_with_read(
728        &self,
729        req: &AuthRequest,
730        read: crate::session::SessionRead,
731    ) -> AuthResult<(crate::wire::UserView, crate::wire::SessionView)> {
732        let resolved = self.session_manager().resolve(req, read).await?;
733        req.set_session_snapshot(resolved.data.clone().map(Into::into))?;
734        let data = resolved.data.ok_or(AuthError::Unauthenticated)?;
735        if !data.user.id.is_undefined() {
736            req.set_server_context("auth.current-user-id", data.user.id.field_value())?;
737        }
738        Ok((data.user, data.session))
739    }
740}
741
742#[cfg(test)]
743mod tests {
744    mod async_body_dispatch;
745
746    use super::*;
747    use crate::entity::AuthUser;
748    use crate::test_store::test_database;
749
750    // Rust-specific surface: plugin infrastructure helpers and request-dispatch helpers in `crates/core::plugin` are Rust library APIs with no direct TS analogue.
751    #[test]
752    fn auth_route_constructors() {
753        let get = AuthRoute::get("/test", "getTest");
754        assert_eq!(get.method, HttpMethod::Get);
755        assert_eq!(get.path, "/test");
756        assert_eq!(get.operation_id, "getTest");
757
758        let post = AuthRoute::post("/create", "createItem");
759        assert_eq!(post.method, HttpMethod::Post);
760
761        let put = AuthRoute::put("/update", "updateItem");
762        assert_eq!(put.method, HttpMethod::Put);
763
764        let delete = AuthRoute::delete("/remove", "deleteItem");
765        assert_eq!(delete.method, HttpMethod::Delete);
766    }
767
768    // Rust-specific surface: plugin infrastructure helpers and request-dispatch helpers in `crates/core::plugin` are Rust library APIs with no direct TS analogue.
769    #[test]
770    fn auth_route_new() {
771        let route = AuthRoute::new(HttpMethod::Patch, "/patch", "patchIt");
772        assert_eq!(route.method, HttpMethod::Patch);
773        assert_eq!(route.path, "/patch");
774    }
775
776    // Rust-specific surface: plugin infrastructure helpers and request-dispatch helpers in `crates/core::plugin` are Rust library APIs with no direct TS analogue.
777    #[test]
778    fn auth_context_new() {
779        let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
780        let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
781        let db = runtime.block_on(test_database());
782        let ctx = AuthContext::new(config.clone(), db);
783        assert!(ctx.email_provider.is_none());
784        assert!(ctx.metadata.is_empty());
785    }
786
787    // Rust-specific surface: plugin infrastructure helpers and request-dispatch helpers in `crates/core::plugin` are Rust library APIs with no direct TS analogue.
788    #[test]
789    fn auth_context_metadata() {
790        let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
791        let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
792        let db = runtime.block_on(test_database());
793        let mut ctx = AuthContext::new(config, db);
794
795        ctx.set_metadata("key", serde_json::json!("value"));
796        assert_eq!(ctx.get_metadata("key"), Some(&serde_json::json!("value")));
797        assert!(ctx.get_metadata("missing").is_none());
798    }
799
800    // Rust-specific surface: plugin infrastructure helpers and request-dispatch helpers in `crates/core::plugin` are Rust library APIs with no direct TS analogue.
801    #[test]
802    fn auth_context_email_provider_error_when_none() {
803        let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
804        let runtime = tokio::runtime::Runtime::new().expect("runtime should build");
805        let db = runtime.block_on(test_database());
806        let ctx = AuthContext::new(config, db);
807        assert!(ctx.email_provider().is_err());
808    }
809
810    // Rust-specific surface: plugin infrastructure helpers and request-dispatch helpers in `crates/core::plugin` are Rust library APIs with no direct TS analogue.
811    #[tokio::test]
812    async fn auth_context_require_session_unauthenticated() {
813        let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
814        let db = test_database().await;
815        let ctx = AuthContext::new(config, db);
816        let req = AuthRequest::new(HttpMethod::Get, "/test");
817        let result = ctx.require_session(&req).await;
818        assert!(result.is_err());
819    }
820
821    // Rust-specific surface: plugin infrastructure helpers and request-dispatch helpers in `crates/core::plugin` are Rust library APIs with no direct TS analogue.
822    #[tokio::test]
823    async fn auth_context_require_session_with_valid_session() {
824        let config = Arc::new(AuthConfig::new("test-secret-min-32-chars-1234567"));
825        let db = test_database().await;
826
827        // Create a user
828        let user = db
829            .create_user(crate::types::CreateUser::new().with_email("test@test.com"))
830            .await
831            .unwrap();
832
833        // Create a session
834        let sm = SessionManager::new(config.clone(), db.clone());
835        let session = sm.create_session(&user, None, None).await.unwrap();
836
837        // Build request with the session token
838        let ctx = AuthContext::new(config.clone(), db);
839        let mut req = AuthRequest::new(HttpMethod::Get, "/test");
840        let _ = req.headers.insert(
841            "cookie".into(),
842            format!(
843                "better-auth.session_token={}",
844                crate::utils::cookie_utils::sign_cookie_value(
845                    &session.token,
846                    config.signing_secret()
847                )
848            ),
849        );
850
851        let (found_user, _found_session) = ctx.require_session(&req).await.unwrap();
852        assert_eq!(found_user.id(), user.id());
853    }
854}
855
856impl<S: AuthSchema> AuthInitParts<S> {
857    /// Preserve the initialized trust snapshot and auth-instance identity in the final context.
858    pub fn apply_request_runtime(&self, context: &mut AuthContext<S>) {
859        context.request_runtime = self.request_runtime.clone();
860    }
861}