Skip to main content

better_auth_core/plugin/
contracts.rs

1use super::*;
2/// A plugin override for delivery of the core email-verification challenge.
3#[async_trait]
4pub trait VerificationEmailOverride<S: AuthSchema>: Send + Sync {
5    async fn send(
6        &self,
7        user: &crate::wire::UserView,
8        request: Option<&AuthRequest>,
9        ctx: &AuthContext<S>,
10    ) -> AuthResult<()>;
11
12    async fn send_in_transaction(
13        &self,
14        user: &crate::wire::UserView,
15        request: Option<&AuthRequest>,
16        ctx: &AuthContext<S>,
17        _transaction: &dyn crate::store::AuthTransaction<S>,
18    ) -> AuthResult<()> {
19        self.send(user, request, ctx).await
20    }
21}
22
23pub struct VerificationEmailOverrideHandle<S: AuthSchema>(
24    pub Arc<dyn VerificationEmailOverride<S>>,
25);
26
27/// Action returned by [`AuthPlugin::before_request`].
28#[derive(Debug)]
29#[expect(
30    clippy::large_enum_variant,
31    reason = "Preserve public InjectSession construction as session views gain configured output policies"
32)]
33pub enum BeforeRequestAction {
34    /// Short-circuit with this response (e.g. return session JSON).
35    Respond(AuthResponse),
36    /// Inject a virtual session so downstream handlers see it as authenticated.
37    InjectSession { session: crate::wire::SessionView },
38    /// Replace headers for subsequent hooks, endpoint dispatch and response hooks.
39    /// Authentication adapters can establish a verified signed cookie while
40    /// retaining normal session lookup, expiry and revocation checks.
41    ReplaceHeaders { headers: HashMap<String, String> },
42}
43
44/// Physical HTTP hook control flow, before route selection and endpoint hooks.
45#[derive(Debug)]
46pub enum HttpRequestAction {
47    /// Finish transport processing without endpoint or HTTP response hooks.
48    Respond(AuthResponse),
49    /// Replace physical input for later HTTP hooks and route selection.
50    /// Dispatch discards session state, extensions and queued headers on this value.
51    ReplaceRequest(Box<AuthRequest>),
52}
53
54/// Endpoint output distinguishing a logical value from a native raw response.
55/// Raw responses bypass completed endpoint hooks, while HTTP response hooks
56/// still observe them, matching a Source endpoint returning a `Response`.
57#[derive(Debug)]
58pub enum HttpEndpointResponse {
59    Value(AuthResponse),
60    Raw(AuthResponse),
61}
62
63/// Plugin trait that all authentication plugins must implement.
64///
65#[async_trait]
66pub trait AuthPlugin<S: AuthSchema>: Send + Sync {
67    /// Plugin name - should be unique
68    fn name(&self) -> &'static str;
69
70    /// Routes that this plugin handles
71    fn routes(&self) -> Vec<AuthRoute>;
72
73    /// Ordered rate-limit policies supplied by this installed plugin.
74    fn rate_limits(&self) -> Vec<crate::middleware::rate_limit::PluginRateLimit> {
75        Vec::new()
76    }
77
78    /// Trusted operations available through host dispatch, independently of HTTP visibility.
79    fn server_endpoints(&self) -> Vec<crate::endpoint::EndpointDefinition> {
80        Vec::new()
81    }
82
83    /// Installed logical-call hooks, in this plugin's registration order.
84    fn endpoint_hooks(&self) -> Vec<&dyn crate::endpoint::EndpointHook<S>> {
85        Vec::new()
86    }
87
88    /// Validate actual input after all before-hook patches have been applied.
89    /// # Errors
90    /// Returns an intentional validation error, which completed hooks can observe.
91    fn validate_endpoint(
92        &self,
93        call: &crate::endpoint::EndpointCall,
94        _ctx: &AuthContext<S>,
95    ) -> AuthResult<crate::endpoint::EndpointInput> {
96        Ok(crate::endpoint::EndpointInput {
97            body: call.body().cloned(),
98            query: call.query().cloned(),
99        })
100    }
101
102    /// Execute a registered operation with validated handler-phase context.
103    /// # Errors
104    /// Returns the operation's actual API or application failure.
105    async fn on_endpoint(
106        &self,
107        _call: &crate::endpoint::EndpointCall,
108        _ctx: &AuthContext<S>,
109    ) -> AuthResult<crate::endpoint::EndpointResponse> {
110        Err(AuthError::not_implemented(
111            "This plugin does not implement trusted endpoint calls",
112        ))
113    }
114
115    /// Media types accepted before request hooks and endpoint dispatch. An empty
116    /// list disables the media restriction for an application-owned endpoint.
117    fn allowed_media_types(&self, _route: &AuthRoute) -> Vec<&'static str> {
118        vec!["application/json"]
119    }
120
121    /// Session field policies contributed by this registered plugin.
122    fn session_fields(&self) -> indexmap::IndexMap<String, crate::field_policy::FieldConfig> {
123        indexmap::IndexMap::new()
124    }
125
126    fn user_fields(&self) -> crate::field_policy::FieldConfigs {
127        crate::field_policy::FieldConfigs::new()
128    }
129
130    fn account_fields(&self) -> crate::field_policy::FieldConfigs {
131        crate::field_policy::FieldConfigs::new()
132    }
133
134    /// Documentation annotations collected after all plugins initialize.
135    /// Override this hook for custom endpoints and model field policies.
136    fn openapi_metadata(&self, _ctx: &AuthInitContext<S>) -> crate::openapi::PluginOpenApiMetadata {
137        self.static_openapi_metadata()
138    }
139
140    /// Base declarations for documentation without an initialized auth instance.
141    fn static_openapi_metadata(&self) -> crate::openapi::PluginOpenApiMetadata {
142        crate::openapi::annotations::route_metadata(&self.routes())
143    }
144
145    /// Called when the plugin is initialized
146    async fn on_init(&self, _ctx: &mut AuthInitContext<S>) -> AuthResult<()> {
147        Ok(())
148    }
149
150    /// Inspect the original physical HTTP request before routing, body parsing,
151    /// origin validation and endpoint hooks. Disabled paths and transport/rate
152    /// limiting middleware run first. Returning a response stops dispatch and
153    /// later plugin hooks. Server-only endpoint calls do not invoke this hook.
154    async fn on_http_request(
155        &self,
156        _req: &AuthRequest,
157        _ctx: &AuthContext<S>,
158    ) -> AuthResult<Option<AuthResponse>> {
159        Ok(None)
160    }
161
162    /// Inspect or replace the physical request. The default preserves existing
163    /// `on_http_request` implementations. Request-local configuration has already
164    /// been resolved from the incoming request, matching the Source HTTP handler.
165    async fn on_http_request_action(
166        &self,
167        req: &AuthRequest,
168        ctx: &AuthContext<S>,
169    ) -> AuthResult<Option<HttpRequestAction>> {
170        Ok(self
171            .on_http_request(req, ctx)
172            .await?
173            .map(HttpRequestAction::Respond))
174    }
175
176    /// Observe the routed HTTP response before transport middleware (including CORS).
177    /// Hooks run in registration order; the first replacement stops this chain.
178    /// Early HTTP request-hook responses skip this stage. Errors escape dispatch;
179    /// they cannot reverse writes already committed by an endpoint.
180    async fn on_http_response(
181        &self,
182        _req: &AuthRequest,
183        _ctx: &AuthContext<S>,
184        _response: &AuthResponse,
185    ) -> AuthResult<Option<AuthResponse>> {
186        Ok(None)
187    }
188
189    /// Called after route matching and before endpoint dispatch.
190    ///
191    /// Return `Some(BeforeRequestAction::Respond(..))` to short-circuit with a
192    /// response, `Some(BeforeRequestAction::InjectSession { .. })` to attach a
193    /// virtual session (e.g. API-key → session emulation),
194    /// `Some(BeforeRequestAction::ReplaceHeaders { .. })` to transform request
195    /// headers, or `None` to continue endpoint dispatch.
196    async fn before_request(
197        &self,
198        _req: &AuthRequest,
199        _ctx: &AuthContext<S>,
200    ) -> AuthResult<Option<BeforeRequestAction>> {
201        Ok(None)
202    }
203
204    /// Called for each request - return Some(response) to handle, None to pass through
205    async fn on_request(
206        &self,
207        req: &AuthRequest,
208        ctx: &AuthContext<S>,
209    ) -> AuthResult<Option<AuthResponse>>;
210
211    /// Dispatch the resolved HTTP endpoint. Existing plugins return logical
212    /// values through `on_request`; plugins returning a native raw response can
213    /// override this method to bypass the endpoint after-hook pipeline.
214    async fn on_http_endpoint(
215        &self,
216        req: &AuthRequest,
217        ctx: &AuthContext<S>,
218    ) -> AuthResult<Option<HttpEndpointResponse>> {
219        Ok(self
220            .on_request(req, ctx)
221            .await?
222            .map(HttpEndpointResponse::Value))
223    }
224
225    /// Transform a completed response, including redirects and rejections.
226    ///
227    /// Hooks run in plugin registration order with the normalized auth path.
228    /// This supports cookie and header lifecycles that span other plugins.
229    async fn after_request(
230        &self,
231        _req: &AuthRequest,
232        _ctx: &AuthContext<S>,
233        response: AuthResponse,
234    ) -> AuthResult<AuthResponse> {
235        Ok(response)
236    }
237
238    /// Called after a user is created
239    async fn on_user_created(&self, _user: &S::User, _ctx: &AuthContext<S>) -> AuthResult<()> {
240        Ok(())
241    }
242
243    /// Called after a session is created
244    async fn on_session_created(
245        &self,
246        _session: &S::Session,
247        _ctx: &AuthContext<S>,
248    ) -> AuthResult<()> {
249        Ok(())
250    }
251
252    /// Called before a user is deleted
253    async fn on_user_deleted(&self, _user_id: &str, _ctx: &AuthContext<S>) -> AuthResult<()> {
254        Ok(())
255    }
256
257    /// Called before a session is deleted
258    async fn on_session_deleted(
259        &self,
260        _session_token: &str,
261        _ctx: &AuthContext<S>,
262    ) -> AuthResult<()> {
263        Ok(())
264    }
265}
266
267impl<S: AuthSchema> std::fmt::Debug for VerificationEmailOverrideHandle<S> {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        f.debug_struct("VerificationEmailOverrideHandle")
270            .finish_non_exhaustive()
271    }
272}