better_auth_core/request_runtime/
mod.rs1mod config;
4mod cookies;
5mod scope;
6mod url;
7
8pub use config::{BaseUrl, BaseUrlProtocol, DynamicBaseUrl, TrustedValues, TrustedValuesResolver};
9pub use cookies::{CookieSettings, ResolvedCookie};
10pub(crate) use scope::{Identity as RuntimeIdentity, spawn as spawn_with_request_context};
11
12use std::collections::HashMap;
13use std::future::Future;
14use std::sync::Arc;
15
16use self::url::Source;
17use crate::{
18 AuthConfig, AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, AuthSchema,
19 CookieAttributes, HttpMethod,
20};
21
22#[derive(Clone, Copy, Default)]
24pub struct NativeRequest<'a> {
25 pub request: Option<&'a AuthRequest>,
26 pub headers: Option<&'a HashMap<String, String>>,
27}
28
29#[derive(Clone, Default)]
30pub(crate) struct RequestRuntime {
31 identity: scope::Identity,
32 initial: Arc<tokio::sync::OnceCell<Initialized>>,
33 values: Option<RuntimeValues>,
34}
35
36#[derive(Clone)]
37struct Initialized {
38 config: Arc<AuthConfig>,
39 values: RuntimeValues,
40}
41
42#[derive(Clone)]
43struct RuntimeValues {
44 base_url: String,
45 trusted_origins: Vec<String>,
46 trusted_providers: Vec<String>,
47 is_production: bool,
48}
49
50impl RequestRuntime {
51 async fn initialize(config: &mut AuthConfig) -> AuthResult<RuntimeValues> {
53 if let BaseUrl::Dynamic(policy) = &config.base_url
54 && policy.allowed_hosts.is_empty()
55 {
56 return Err(AuthError::config(
57 "baseURL.allowedHosts cannot be empty. Provide at least one allowed host pattern (e.g., [\"myapp.com\", \"*.vercel.app\"]).",
58 ));
59 }
60 let base_url = match &config.base_url {
61 BaseUrl::Dynamic(_) => String::new(),
62 BaseUrl::Auto | BaseUrl::Static(_) => {
63 let environment = url::environment_base_url()?;
64 let value = url::static_url(
65 config.base_url.as_static(),
66 &config.base_path,
67 None,
68 environment.as_deref(),
69 false,
70 )
71 .map_err(AuthError::config)?
72 .unwrap_or_default();
73 config.base_url = BaseUrl::Static(url::origin(&value).unwrap_or_default());
75 value
76 }
77 };
78 if config.base_path.is_empty() {
79 config.base_path = "/api/auth".to_owned();
80 }
81 let is_production = url::environment("NODE_ENV")?.as_deref() == Some("production");
82 let cookies = CookieSettings::from_config(config, is_production);
83 if config
84 .advanced
85 .cross_sub_domain_cookies
86 .as_ref()
87 .is_some_and(|policy| {
88 policy.enabled() && policy.domain.as_deref().is_none_or(str::is_empty)
89 })
90 && !matches!(config.base_url, BaseUrl::Dynamic(_))
91 && config.base_url.as_static().is_none_or(str::is_empty)
92 {
93 return Err(AuthError::config(
94 "baseURL is required when crossSubdomainCookies are enabled.",
95 ));
96 }
97 config.install_cookie_settings(cookies.clone());
98 let trusted_origins = origins(config, &config.base_url, None).await?;
99 let trusted_providers = config
100 .account
101 .account_linking
102 .trusted_provider_values()
103 .resolve(None)
104 .await?;
105 Ok(RuntimeValues {
106 base_url,
107 trusted_origins,
108 trusted_providers,
109 is_production,
110 })
111 }
112}
113
114impl<S: AuthSchema> AuthContext<S> {
115 pub fn current_request_context(&self) -> Option<Arc<Self>> {
117 self.request_runtime.identity.current()
118 }
119
120 pub fn is_origin_trusted(&self, origin: &str) -> bool {
121 self.trusted_origins().iter().any(|pattern| {
122 let pattern = crate::config::extract_origin(pattern).unwrap_or_default();
123 glob_match::glob_match(&pattern, origin)
124 })
125 }
126
127 pub fn is_redirect_target_trusted(&self, target: &str) -> bool {
128 crate::config::is_safe_relative_path(target)
129 || crate::config::extract_origin(target)
130 .is_some_and(|origin| self.is_origin_trusted(&origin))
131 }
132
133 pub fn base_url(&self) -> &str {
135 self.request_runtime.values.as_ref().map_or_else(
136 || self.config.base_url.as_static().unwrap_or(""),
137 |values| values.base_url.as_str(),
138 )
139 }
140
141 pub fn base_path(&self) -> &str {
143 let url = self.base_url();
144 url.find("://")
145 .and_then(|scheme| url.get(scheme + 3..))
146 .and_then(|authority| authority.find('/').and_then(|start| authority.get(start..)))
147 .map(|path| path.split(['?', '#']).next().unwrap_or("/"))
148 .unwrap_or(&self.config.base_path)
149 }
150
151 pub fn trusted_origins(&self) -> &[String] {
152 self.request_runtime.values.as_ref().map_or_else(
153 || {
154 self.config
155 .trusted_origin_values()
156 .as_static()
157 .unwrap_or(&[])
158 },
159 |values| values.trusted_origins.as_slice(),
160 )
161 }
162
163 pub(crate) fn is_production(&self) -> AuthResult<bool> {
164 Ok(self.runtime_values()?.is_production)
165 }
166
167 pub fn trusted_providers(&self) -> &[String] {
168 self.request_runtime.values.as_ref().map_or_else(
169 || {
170 self.config
171 .account
172 .account_linking
173 .trusted_provider_values()
174 .as_static()
175 .unwrap_or(&[])
176 },
177 |values| values.trusted_providers.as_slice(),
178 )
179 }
180
181 pub fn create_auth_cookie(
182 &self,
183 name: &str,
184 attributes: CookieAttributes,
185 ) -> AuthResult<ResolvedCookie> {
186 Ok(self.config.auth_cookie(name, attributes))
187 }
188
189 pub async fn initialize_request_context(&self) -> AuthResult<Arc<Self>> {
191 if self.request_runtime.values.is_some() {
192 return Ok(Arc::new(self.clone()));
193 }
194 let initial = self
195 .request_runtime
196 .initial
197 .get_or_try_init(|| async {
198 let mut config = (*self.config).clone();
199 let values = RequestRuntime::initialize(&mut config).await?;
200 Ok::<_, AuthError>(Initialized {
201 config: Arc::new(config),
202 values,
203 })
204 })
205 .await?;
206 let mut context = self.clone();
207 context.config = initial.config.clone();
208 context.request_runtime.values = Some(initial.values.clone());
209 Ok(Arc::new(context))
210 }
211
212 pub async fn with_native_context<T, F, Fut>(
214 &self,
215 input: NativeRequest<'_>,
216 operation: F,
217 ) -> AuthResult<T>
218 where
219 F: FnOnce(Arc<AuthContext<S>>) -> Fut + Send,
220 Fut: Future<Output = AuthResult<T>> + Send,
221 T: Send,
222 {
223 if let Some(validation) = self.database.schema_validation() {
224 validation.check_runtime().await?;
225 }
226 let context = self.initialize_request_context().await?;
227 let resolved = if matches!(context.config.base_url, BaseUrl::Dynamic(_))
228 && context.base_url().is_empty()
229 {
230 let source = input.request.map(Source::Request).or_else(|| {
231 input
232 .headers
233 .filter(|headers| {
234 url::header(headers, "host").is_some()
235 || url::header(headers, "x-forwarded-host").is_some()
236 })
237 .map(Source::Headers)
238 });
239 context.resolve_dynamic(source, true).await?
240 } else {
241 context
242 };
243 scope::run(resolved.clone(), async move { operation(resolved).await }).await
244 }
245
246 pub async fn with_http_context<T, F, Fut>(
248 &self,
249 request: &AuthRequest,
250 operation: F,
251 ) -> AuthResult<T>
252 where
253 F: FnOnce(Arc<AuthContext<S>>) -> Fut + Send,
254 Fut: Future<Output = AuthResult<T>> + Send,
255 T: Send,
256 {
257 let context = self.initialize_request_context().await?;
258 let resolved = if matches!(context.config.base_url, BaseUrl::Dynamic(_)) {
259 context
260 .resolve_dynamic(Some(Source::Request(request)), false)
261 .await?
262 } else {
263 let mut resolved = (*context).clone();
264 if context
265 .config
266 .base_url
267 .as_static()
268 .is_none_or(str::is_empty)
269 {
270 let environment = url::environment_base_url()?;
271 let base_url = url::static_url(
272 None,
273 &self.config.base_path,
274 Some(request),
275 environment.as_deref(),
276 self.config.advanced.trusted_proxy_headers,
277 )
278 .map_err(AuthError::config)?
279 .ok_or_else(|| {
280 AuthError::config(
281 "Could not get base URL from request. Please provide a valid base URL.",
282 )
283 })?;
284 resolved.runtime_values_mut()?.base_url = base_url.clone();
285 Arc::make_mut(&mut resolved.config).base_url =
286 BaseUrl::Static(url::origin(&base_url).unwrap_or_default());
287 }
288 resolved.runtime_values_mut()?.trusted_origins =
289 origins(&resolved.config, &resolved.config.base_url, Some(request)).await?;
290 resolved.runtime_values_mut()?.trusted_providers = resolved
291 .config
292 .account
293 .account_linking
294 .trusted_provider_values()
295 .resolve(Some(request))
296 .await?;
297 Arc::new(resolved)
298 };
299 scope::run(resolved.clone(), async move { operation(resolved).await }).await
300 }
301
302 fn runtime_values(&self) -> AuthResult<&RuntimeValues> {
303 self.request_runtime
304 .values
305 .as_ref()
306 .ok_or_else(|| AuthError::internal("Request context is not initialized"))
307 }
308
309 fn runtime_values_mut(&mut self) -> AuthResult<&mut RuntimeValues> {
310 self.request_runtime
311 .values
312 .as_mut()
313 .ok_or_else(|| AuthError::internal("Request context is not initialized"))
314 }
315
316 pub(crate) fn runtime_identity(&self) -> RuntimeIdentity {
317 self.request_runtime.identity.clone()
318 }
319
320 pub async fn csrf_trusted_origins(&self, request: &AuthRequest) -> AuthResult<Vec<String>> {
322 let mut values = self.trusted_origins().to_vec();
323 if self.config.trusted_origin_values().is_dynamic() {
324 values.extend(
325 self.config
326 .trusted_origin_values()
327 .resolve(Some(request))
328 .await?,
329 );
330 }
331 Ok(values)
332 }
333
334 async fn resolve_dynamic(
335 &self,
336 source: Option<Source<'_>>,
337 native: bool,
338 ) -> AuthResult<Arc<Self>> {
339 let BaseUrl::Dynamic(policy) = &self.config.base_url else {
340 return Err(AuthError::internal(
341 "Dynamic context resolution requires a dynamic base URL",
342 ));
343 };
344 if native && source.is_none() && policy.fallback.as_deref().is_none_or(str::is_empty) {
345 return Err(native_url_error("Dynamic baseURL could not be resolved for this direct auth.api call. Pass `headers: request.headers` (or `request`) to the call, or add `fallback` to your baseURL config.".to_owned()));
346 }
347 let base_url = url::dynamic_url(
348 policy,
349 &self.config.base_path,
350 source,
351 self.config.advanced.trusted_proxy_headers,
352 )
353 .map_err(|message| {
354 if native {
355 native_url_error(message)
356 } else {
357 AuthError::config(message)
358 }
359 })?;
360 let synthetic;
361 let request = match source {
362 Some(Source::Request(request)) => Some(request),
363 Some(Source::Headers(headers)) => {
364 let url = ::url::Url::parse(&base_url)
365 .map_err(|error| AuthError::config(error.to_string()))?;
366 synthetic = AuthRequest::new(HttpMethod::Get, url.path())
367 .with_optional_headers(Some(headers.clone()))
368 .with_url(url);
369 Some(&synthetic)
370 }
371 None => None,
372 };
373 let mut resolved = self.clone();
374 resolved.runtime_values_mut()?.base_url = base_url.clone();
375 Arc::make_mut(&mut resolved.config).base_url =
376 BaseUrl::Static(url::origin(&base_url).unwrap_or_default());
377 resolved.runtime_values_mut()?.trusted_origins =
379 origins(&resolved.config, &self.config.base_url, request).await?;
380 resolved.runtime_values_mut()?.trusted_providers = resolved
381 .config
382 .account
383 .account_linking
384 .trusted_provider_values()
385 .resolve(request)
386 .await?;
387 if self
388 .config
389 .advanced
390 .cross_sub_domain_cookies
391 .as_ref()
392 .is_some_and(|policy| policy.enabled())
393 {
394 let cookies =
395 CookieSettings::from_config(&resolved.config, self.runtime_values()?.is_production);
396 Arc::make_mut(&mut resolved.config).install_cookie_settings(cookies);
397 }
398 Ok(Arc::new(resolved))
399 }
400}
401
402async fn origins(
403 config: &AuthConfig,
404 base_url: &BaseUrl,
405 request: Option<&AuthRequest>,
406) -> AuthResult<Vec<String>> {
407 let mut values = url::base_origins(base_url);
408 values.extend(config.trusted_origin_values().resolve(request).await?);
409 if let Some(environment) = url::environment("BETTER_AUTH_TRUSTED_ORIGINS")? {
410 values.extend(environment.split(',').map(str::to_owned));
411 }
412 values.retain(|value| !value.is_empty());
413 Ok(values)
414}
415
416fn native_url_error(message: String) -> AuthError {
417 match AuthResponse::json(500, &serde_json::json!({ "message": message })) {
418 Ok(response) => response.into(),
419 Err(error) => error.into(),
420 }
421}
422
423pub(crate) fn current_logger() -> Option<crate::observability::LoggerConfig> {
424 scope::current_logger()
425}