Skip to main content

alibi_core/config/
mod.rs

1mod account;
2mod advanced;
3pub(crate) use advanced::{sso_provider_column, two_factor_column};
4mod client_ip;
5mod id;
6mod identity;
7mod origin;
8mod secrets;
9mod session;
10
11pub use account::{AccountConfig, AccountLinkingConfig, OAuthStateStrategy};
12pub use advanced::{
13    AdvancedConfig, AdvancedDatabaseConfig, CookieAttributes, CookieOverride, CrossSubDomainConfig,
14    IpAddressConfig, SameSite, SsoProviderDatabaseConfig, TwoFactorDatabaseConfig,
15};
16#[doc(hidden)]
17pub use id::serial_id_statements;
18pub use id::{DatabaseIdGenerator, DatabaseIdStrategy};
19pub use identity::{PasswordConfig, UserConfig, VerificationConfig};
20pub use origin::{
21    BaseUrlProtocol, DynamicBaseUrl, TrustedOriginsResolver, TrustedProvidersResolver,
22};
23pub use secrets::ManagedSecrets;
24pub use session::{
25    CookieCacheConfig, CookieCacheStrategy, CookieRefreshCache, JwtConfig, SessionConfig,
26};
27
28/// Well-known core route paths.
29///
30/// These constants are the single source of truth for route paths used by both
31/// the core request dispatcher (`handle_core_request`) and framework-specific
32/// routers (e.g. Axum) so that path strings are never duplicated.
33pub mod core_paths {
34    pub const OK: &str = "/ok";
35    pub const ERROR: &str = "/error";
36    pub const HEALTH: &str = "/health";
37    pub const OPENAPI_SPEC: &str = "/__test/openapi.json";
38    pub const UPDATE_USER: &str = "/update-user";
39    pub const DELETE_USER: &str = "/delete-user";
40    pub const CHANGE_EMAIL: &str = "/change-email";
41    pub const DELETE_USER_CALLBACK: &str = "/delete-user/callback";
42}
43
44use crate::email::EmailProvider;
45use crate::error::AuthError;
46use chrono::Duration;
47use std::collections::HashMap;
48use std::sync::Arc;
49
50/// Response policy for awaited lifecycle notifications. Explicit verification
51/// delivery retains its direct error contract; deferred work logs its errors.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub enum AwaitedNotificationErrorPolicy {
54    /// Propagate the callback error after retaining any committed auth writes.
55    #[default]
56    Propagate,
57    /// Log delivery failure and continue the authentication response.
58    LogAndContinue,
59}
60
61/// Main configuration for `Alibi`
62#[derive(Clone)]
63pub struct AuthConfig {
64    /// Single-secret input. Managed mode uses `current_secret()` instead.
65    pub secret: String,
66
67    /// Versioned encryption keys. When present, its current key signs new tokens.
68    /// Managed ciphertext requires a versioned envelope.
69    pub managed_secrets: Option<ManagedSecrets>,
70
71    /// Application name, used for cookie prefixes, email templates, etc.
72    ///
73    /// Defaults to `"Better Auth"`.
74    pub app_name: String,
75
76    /// Base URL for the authentication service (e.g. `"http://localhost:3000"`).
77    pub base_url: String,
78
79    /// Request-dependent host allowlist. Static `base_url` remains the default.
80    pub dynamic_base_url: Option<DynamicBaseUrl>,
81
82    /// Application callback resolving additional trusted origins from the real request.
83    pub trusted_origins_resolver: Option<Arc<dyn TrustedOriginsResolver>>,
84
85    /// Base path where the auth routes are mounted.
86    ///
87    /// All routes handled by `Alibi` will be prefixed with this path.
88    /// For example, with the default `"/api/auth"`, the sign-in route becomes
89    /// `"/api/auth/sign-in/email"`.
90    ///
91    /// Defaults to `"/api/auth"`.
92    pub base_path: String,
93
94    /// Origins that are trusted for CSRF and other cross-origin checks.
95    ///
96    /// Supports glob patterns (e.g. `"https://*.example.com"`).
97    /// These are shared across all middleware that needs origin validation
98    /// (CSRF, CORS, etc.).
99    pub trusted_origins: Vec<String>,
100
101    /// Paths that should be disabled (skipped) by the router.
102    ///
103    /// Any request whose path matches an entry in this list will receive
104    /// a 404 response, even if a handler is registered for it.
105    pub disabled_paths: Vec<String>,
106
107    /// Render the built-in HTML page for `GET /error` instead of redirecting to `/`.
108    ///
109    /// Defaults to `false` when `NODE_ENV` is `production`, and `true` otherwise.
110    /// Set this to `true` to enable the renderer in production, corresponding to
111    /// upstream's explicit `onAPIError.customizeDefaultErrorPage` option.
112    pub render_error_page: bool,
113
114    /// Default OAuth error destination, corresponding to `onAPIError.errorURL`.
115    pub api_error_url: Option<String>,
116
117    /// Propagate ordinary endpoint failures to the hosting application.
118    /// Explicit public API errors continue to return HTTP responses.
119    pub throw_api_errors: bool,
120
121    /// Session configuration
122    pub session: SessionConfig,
123
124    /// Application user-field input, adapter and public output policies.
125    pub user: UserConfig,
126
127    /// Verification storage lifecycle settings.
128    pub verification: VerificationConfig,
129
130    /// JWT configuration
131    pub jwt: JwtConfig,
132
133    /// Password configuration
134    pub password: PasswordConfig,
135
136    /// Account configuration (linking, token encryption, etc.)
137    pub account: AccountConfig,
138
139    /// Email provider for sending emails (verification, password reset, etc.)
140    pub email_provider: Option<Arc<dyn EmailProvider>>,
141    /// Observe already running deferred operations. Ignoring their completion
142    /// does not cancel them; applications may retain completions for shutdown.
143    pub background_tasks: Option<Arc<dyn crate::BackgroundTaskHandler>>,
144    /// Error handling when the request awaits a lifecycle notification.
145    pub awaited_notification_errors: AwaitedNotificationErrorPolicy,
146
147    /// Validate fresh identity data before its creation hooks or provider
148    /// account/session writes. Returning non-provider sign-ins are unchanged.
149    pub user_validation: Option<Arc<dyn crate::user_validation::UserInfoValidator>>,
150
151    /// Advanced configuration options
152    pub advanced: AdvancedConfig,
153}
154
155impl Default for AuthConfig {
156    fn default() -> Self {
157        Self {
158            secret: String::new(),
159            managed_secrets: None,
160            app_name: "Better Auth".to_owned(),
161            base_url: "http://localhost:3000".to_owned(),
162            dynamic_base_url: None,
163            trusted_origins_resolver: None,
164            base_path: "/api/auth".to_owned(),
165            trusted_origins: Vec::new(),
166            disabled_paths: Vec::new(),
167            render_error_page: !std::env::var("NODE_ENV").is_ok_and(|value| value == "production"),
168            api_error_url: None,
169            throw_api_errors: false,
170            session: SessionConfig::default(),
171            user: UserConfig::default(),
172            verification: VerificationConfig::default(),
173            jwt: JwtConfig::default(),
174            password: PasswordConfig::default(),
175            account: AccountConfig::default(),
176            email_provider: None,
177            background_tasks: None,
178            awaited_notification_errors: AwaitedNotificationErrorPolicy::default(),
179            user_validation: None,
180            advanced: AdvancedConfig::default(),
181        }
182    }
183}
184
185impl AuthConfig {
186    /// Choose whether awaited lifecycle email delivery failures fail the request.
187    #[must_use]
188    pub const fn awaited_notification_errors(
189        mut self,
190        policy: AwaitedNotificationErrorPolicy,
191    ) -> Self {
192        self.awaited_notification_errors = policy;
193        self
194    }
195
196    /// Integrate deferred task completions with the application executor.
197    #[must_use]
198    pub fn background_tasks(mut self, handler: Arc<dyn crate::BackgroundTaskHandler>) -> Self {
199        self.background_tasks = Some(handler);
200        self
201    }
202
203    #[must_use]
204    pub fn new(secret: impl Into<String>) -> Self {
205        Self {
206            secret: secret.into(),
207            ..Default::default()
208        }
209    }
210
211    /// Set the application name.
212    #[must_use]
213    pub fn app_name(mut self, name: impl Into<String>) -> Self {
214        self.app_name = name.into();
215        self
216    }
217
218    /// Set the base URL (e.g. `"https://myapp.com"`).
219    ///
220    /// Also updates `session.cookie_secure` to match the URL scheme:
221    /// HTTPS URLs set `Secure=true`, HTTP URLs set `Secure=false`.
222    #[must_use]
223    pub fn base_url(mut self, url: impl Into<String>) -> Self {
224        self.base_url = url.into();
225        self.session.cookie_secure = self.base_url.starts_with("https://");
226        self
227    }
228
229    #[must_use]
230    pub fn account(mut self, account: AccountConfig) -> Self {
231        self.account = account;
232        self
233    }
234
235    /// Configure the default OAuth error destination (`onAPIError.errorURL`).
236    /// An empty value uses the endpoint's ordinary fallback.
237    #[must_use]
238    pub fn api_error_url(mut self, url: impl Into<String>) -> Self {
239        self.api_error_url = Some(url.into());
240        self
241    }
242
243    /// Set the base path where auth routes are mounted.
244    #[must_use]
245    pub fn base_path(mut self, path: impl Into<String>) -> Self {
246        self.base_path = path.into();
247        self
248    }
249
250    /// Add a trusted origin. Supports glob patterns (e.g. `"https://*.example.com"`).
251    #[must_use]
252    pub fn trusted_origin(mut self, origin: impl Into<String>) -> Self {
253        self.trusted_origins.push(origin.into());
254        self
255    }
256
257    /// Set all trusted origins at once.
258    #[must_use]
259    pub fn trusted_origins(mut self, origins: Vec<String>) -> Self {
260        self.trusted_origins = origins;
261        self
262    }
263
264    /// Add a path to the disabled paths list.
265    #[must_use]
266    pub fn disabled_path(mut self, path: impl Into<String>) -> Self {
267        self.disabled_paths.push(path.into());
268        self
269    }
270
271    /// Set all disabled paths at once.
272    #[must_use]
273    pub fn disabled_paths(mut self, paths: Vec<String>) -> Self {
274        self.disabled_paths = paths;
275        self
276    }
277
278    /// Propagate ordinary endpoint exceptions through the public request result.
279    #[must_use]
280    pub const fn throw_api_errors(mut self, enabled: bool) -> Self {
281        self.throw_api_errors = enabled;
282        self
283    }
284
285    /// Set the session expiration duration.
286    #[must_use]
287    pub const fn session_expires_in(mut self, duration: Duration) -> Self {
288        self.session.expires_in = duration;
289        self
290    }
291
292    #[must_use]
293    pub const fn session_update_age(mut self, duration: Duration) -> Self {
294        self.session.update_age = Some(duration);
295        self
296    }
297
298    #[must_use]
299    pub const fn disable_session_refresh(mut self, disabled: bool) -> Self {
300        self.session.disable_session_refresh = disabled;
301        self
302    }
303
304    #[must_use]
305    pub const fn session_fresh_age(mut self, duration: Duration) -> Self {
306        self.session.fresh_age = Some(duration);
307        self
308    }
309
310    /// Set the cookie cache configuration for sessions.
311    #[must_use]
312    pub fn session_cookie_cache(mut self, config: CookieCacheConfig) -> Self {
313        self.session.cookie_cache = Some(config);
314        self
315    }
316
317    /// Set the JWT expiration duration.
318    #[must_use]
319    pub const fn jwt_expires_in(mut self, duration: Duration) -> Self {
320        self.jwt.expires_in = duration;
321        self
322    }
323
324    #[must_use]
325    pub fn advanced(mut self, advanced: AdvancedConfig) -> Self {
326        self.advanced = advanced;
327        self
328    }
329
330    #[must_use]
331    pub fn cookie_prefix(mut self, prefix: impl Into<String>) -> Self {
332        self.advanced.cookie_prefix = Some(prefix.into());
333        self
334    }
335
336    #[must_use]
337    pub const fn disable_csrf_check(mut self, disabled: bool) -> Self {
338        self.advanced.disable_csrf_check = Some(disabled);
339        self
340    }
341
342    /// Current key for signing tokens and cookies. Retained keys are encryption
343    /// readers; signed cookies and signed JWTs deliberately use only this key.
344    #[must_use]
345    pub fn current_secret(&self) -> &str {
346        self.managed_secrets
347            .as_ref()
348            .map_or(self.secret.as_str(), ManagedSecrets::current_secret)
349    }
350
351    /// Current-format encryption readers in configured order.
352    pub fn verification_secrets(&self) -> impl Iterator<Item = &str> {
353        let keys = self
354            .managed_secrets
355            .as_ref()
356            .map(secrets::ManagedSecrets::verification_secrets);
357        keys.into_iter().flatten().chain(
358            self.managed_secrets
359                .is_none()
360                .then_some(self.secret.as_str()),
361        )
362    }
363
364    /// Enable managed encryption with an explicit current version and readers.
365    #[must_use]
366    pub fn managed_secrets(mut self, secrets: ManagedSecrets) -> Self {
367        self.managed_secrets = Some(secrets);
368        self
369    }
370
371    /// Determine whether this request path opts out of origin validation.
372    #[must_use]
373    pub fn origin_check_disabled_for(&self, path: &str) -> bool {
374        if self.advanced.disable_origin_check {
375            return true;
376        }
377        let path = path
378            .strip_prefix(self.base_path.trim_end_matches('/'))
379            .filter(|suffix| suffix.starts_with('/'))
380            .unwrap_or(path)
381            .trim_end_matches('/');
382        self.advanced.disable_origin_check_paths.iter().any(|skip| {
383            let skip = skip.trim_end_matches('/');
384            path == skip
385                || path
386                    .strip_prefix(skip)
387                    .is_some_and(|suffix| suffix.starts_with('/'))
388        })
389    }
390
391    /// Apply the configured origin policy at an initialized request callback.
392    #[must_use]
393    pub fn current_origin_check_disabled(&self) -> bool {
394        crate::hooks::current_request_hook_context()
395            .map_or(self.advanced.disable_origin_check, |request| {
396                self.origin_check_disabled_for(&request.path)
397            })
398    }
399
400    #[must_use]
401    pub const fn disable_origin_check(mut self, disabled: bool) -> Self {
402        self.advanced.disable_origin_check = disabled;
403        self
404    }
405
406    #[must_use]
407    pub fn cross_sub_domain_cookies(mut self, domain: impl Into<String>) -> Self {
408        self.advanced.cross_sub_domain_cookies = Some(CrossSubDomainConfig {
409            domain: domain.into(),
410        });
411        self
412    }
413
414    /// Share cookies using the configured or request-resolved base URL's hostname.
415    /// This follows the published hostname directly; it does not infer a parent
416    /// or registrable domain. Use [`cross_sub_domain_cookies`](Self::cross_sub_domain_cookies)
417    /// when a deployment needs an explicit parent domain.
418    #[must_use]
419    pub fn cross_sub_domain_cookies_from_base_url(mut self) -> Self {
420        self.advanced.cross_sub_domain_cookies = Some(CrossSubDomainConfig::default());
421        self
422    }
423
424    /// Check the resolved base origin or a configured trusted pattern.
425    ///
426    /// Explicit patterns follow Better Auth 1.7.6: `*` and `?` match URL origins
427    /// (or hosts for patterns without a scheme), while custom schemes retain
428    /// authority and normalized path-prefix constraints. Resolve dynamic policies
429    /// with [`resolve_request`](Self::resolve_request) before evaluating them.
430    #[must_use]
431    pub fn is_origin_trusted(&self, origin: &str) -> bool {
432        if self.dynamic_base_url.is_none()
433            && let Some(base_origin) = extract_origin(&self.base_url)
434            && (origin == base_origin
435                || (self.base_url.split_once(':').is_some_and(|(scheme, _)| {
436                    scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
437                }) && extract_origin(origin).as_deref() == Some(base_origin.as_str())))
438        {
439            return true;
440        }
441        self.trusted_origins
442            .iter()
443            .any(|pattern| origin::matches_origin(origin, pattern))
444    }
445
446    /// Check whether a URL is a safe redirect target.
447    ///
448    /// A URL is safe if it is a relative path (starts with `/`, no
449    /// traversal tricks) or its origin matches [`base_url`](Self::base_url)
450    /// or [`trusted_origins`](Self::trusted_origins).
451    ///
452    /// This is used by both the CSRF middleware (for POST body/query
453    /// targets) and per-endpoint origin checks (e.g. verify-email GET).
454    #[must_use]
455    pub fn is_redirect_target_trusted(&self, url: &str) -> bool {
456        is_safe_relative_path(url) || self.is_origin_trusted(url)
457    }
458
459    /// Check whether a given path is disabled.
460    #[must_use]
461    pub fn is_path_disabled(&self, path: &str) -> bool {
462        self.disabled_paths.iter().any(|disabled| disabled == path)
463    }
464
465    /// # Errors
466    ///
467    /// Returns a configuration error if the signing secret is empty or shorter than 32 bytes.
468    pub fn validate(&self) -> Result<(), AuthError> {
469        if self.session.stateless
470            && (self.session.secondary_storage.is_some()
471                || self.session.store_in_database
472                || self.session.preserve_in_database)
473        {
474            return Err(AuthError::config(
475                "Stateless sessions cannot use server session storage",
476            ));
477        }
478        if self
479            .dynamic_base_url
480            .as_ref()
481            .is_some_and(|policy| policy.allowed_hosts.is_empty())
482        {
483            return Err(AuthError::config(
484                "Dynamic base URL allowed hosts cannot be empty",
485            ));
486        }
487        if let Some(secrets) = &self.managed_secrets {
488            return secrets.validate();
489        }
490        if self.secret.is_empty() {
491            return Err(AuthError::config("Secret key cannot be empty"));
492        }
493
494        if self.secret.len() < 32 {
495            return Err(AuthError::config(
496                "Secret key must be at least 32 characters",
497            ));
498        }
499
500        Ok(())
501    }
502}
503
504impl std::fmt::Debug for AuthConfig {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        f.debug_struct("AuthConfig").finish_non_exhaustive()
507    }
508}
509
510/// Check whether a URL is a safe relative path.
511///
512/// A relative path is safe if it starts with a single `/` and has no
513/// traversal or scheme-escape tricks (`//`, `\`, `%2f`, `%5c`).
514///
515/// This is used by [`AuthConfig::is_redirect_target_trusted`] and the
516/// CSRF middleware.
517#[must_use]
518pub fn is_safe_relative_path(value: &str) -> bool {
519    if !value.starts_with('/')
520        || value.starts_with("//")
521        || value.contains('\\')
522        || value
523            .chars()
524            .any(|character| matches!(character, '\u{0000}'..='\u{001f}' | '\u{007f}'..='\u{009f}'))
525    {
526        return false;
527    }
528
529    let path = value.split(['?', '#']).next().unwrap_or(value);
530    let lower = path.to_ascii_lowercase();
531    !lower.contains("%2f")
532        && !lower.contains("%5c")
533        && url::Url::parse("https://better-auth.invalid")
534            .and_then(|base| base.join(value))
535            .is_ok_and(|url| url.origin().ascii_serialization() == "https://better-auth.invalid")
536}
537
538/// Extract the origin (scheme + host + port) from a URL string.
539///
540/// For example, `"https://example.com/path"` → `"https://example.com"`.
541///
542/// This is used by [`AuthConfig::is_origin_trusted`] and the CSRF middleware
543/// so that origin comparison is centralised in one place.
544#[must_use]
545pub fn extract_origin(url: &str) -> Option<String> {
546    if let Ok(parsed) = url::Url::parse(url)
547        && matches!(parsed.scheme(), "http" | "https")
548    {
549        return match parsed.origin() {
550            url::Origin::Tuple(..) => Some(parsed.origin().ascii_serialization()),
551            url::Origin::Opaque(_) => None,
552        };
553    }
554    // Custom schemes retain the existing Rust contract. Their upstream
555    // authority/path matcher is a separate capability.
556    pattern_origin(url)
557}
558
559// Configured glob/custom-scheme matching keeps its existing contract; avoid
560// normalizing an explicit pattern into a newly trusted authority.
561fn pattern_origin(url: &str) -> Option<String> {
562    let scheme_end = url.find("://")?;
563    let rest = url.get(scheme_end + 3..)?;
564    let host_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
565    let origin = format!("{}{}", url.get(..scheme_end + 3)?, rest.get(..host_end)?);
566    Some(origin)
567}
568
569// LCOV_EXCL_START
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
575    #[test]
576    fn extract_origin_with_path() {
577        assert_eq!(
578            extract_origin("https://example.com/path"),
579            Some("https://example.com".to_owned())
580        );
581    }
582
583    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
584    #[test]
585    fn extract_origin_without_path() {
586        assert_eq!(
587            extract_origin("https://example.com"),
588            Some("https://example.com".to_owned())
589        );
590    }
591
592    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
593    #[test]
594    fn extract_origin_with_port() {
595        assert_eq!(
596            extract_origin("http://localhost:3000/api"),
597            Some("http://localhost:3000".to_owned())
598        );
599    }
600
601    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
602    #[test]
603    fn extract_origin_with_query() {
604        assert_eq!(
605            extract_origin("https://example.com?foo=bar"),
606            Some("https://example.com".to_owned())
607        );
608    }
609
610    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
611    #[test]
612    fn extract_origin_with_fragment() {
613        assert_eq!(
614            extract_origin("https://example.com#fragment"),
615            Some("https://example.com".to_owned())
616        );
617    }
618
619    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
620    #[test]
621    fn extract_origin_no_scheme() {
622        assert_eq!(extract_origin("example.com"), None);
623    }
624
625    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
626    #[test]
627    fn new_config_sets_secret() {
628        let cfg = AuthConfig::new("a]secret-that-is-at-least-32-characters-long");
629        assert_eq!(cfg.secret, "a]secret-that-is-at-least-32-characters-long");
630    }
631
632    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
633    #[test]
634    fn new_config_uses_defaults() {
635        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567");
636        assert_eq!(cfg.app_name, "Better Auth");
637        assert_eq!(cfg.base_url, "http://localhost:3000");
638        assert_eq!(cfg.base_path, "/api/auth");
639        assert_eq!(cfg.trusted_origins, Vec::<String>::new());
640    }
641
642    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
643    #[test]
644    fn base_url_sets_cookie_secure_for_https() {
645        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
646        assert!(cfg.session.cookie_secure);
647    }
648
649    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
650    #[test]
651    fn base_url_clears_cookie_secure_for_http() {
652        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
653            .base_url("https://myapp.com")
654            .base_url("http://localhost:3000");
655        assert!(!cfg.session.cookie_secure);
656    }
657
658    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
659    #[test]
660    fn builder_chaining() {
661        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
662            .app_name("MyApp")
663            .base_path("/auth")
664            .disable_csrf_check(true)
665            .disable_origin_check(true)
666            .cookie_prefix("myapp");
667
668        assert_eq!(cfg.app_name, "MyApp");
669        assert_eq!(cfg.base_path, "/auth");
670        assert_eq!(cfg.advanced.disable_csrf_check, Some(true));
671        assert!(cfg.advanced.disable_origin_check);
672        assert_eq!(cfg.advanced.cookie_prefix, Some("myapp".to_owned()));
673    }
674
675    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
676    #[test]
677    fn trusted_origin_appends() {
678        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
679            .trusted_origin("https://a.com")
680            .trusted_origin("https://b.com");
681        assert_eq!(cfg.trusted_origins.len(), 2);
682    }
683
684    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
685    #[test]
686    fn trusted_origins_replaces() {
687        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
688            .trusted_origin("https://old.com")
689            .trusted_origins(vec!["https://new.com".to_owned()]);
690        assert_eq!(cfg.trusted_origins, vec!["https://new.com"]);
691    }
692
693    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
694    #[test]
695    fn disabled_path_appends() {
696        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
697            .disabled_path("/admin")
698            .disabled_path("/debug");
699        assert_eq!(cfg.disabled_paths.len(), 2);
700    }
701
702    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
703    #[test]
704    fn disabled_paths_replaces() {
705        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
706            .disabled_path("/old")
707            .disabled_paths(vec!["/new".to_owned()]);
708        assert_eq!(cfg.disabled_paths, vec!["/new"]);
709    }
710
711    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
712    #[test]
713    fn is_origin_trusted_matches_base_url() {
714        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
715        assert!(cfg.is_origin_trusted("https://myapp.com"));
716        let native_custom =
717            AuthConfig::new("test-secret-min-32-chars-1234567").base_url("myapp://auth");
718        assert!(native_custom.is_origin_trusted("myapp://auth"));
719        assert!(!native_custom.is_origin_trusted("myapp://auth/callback"));
720    }
721
722    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
723    #[test]
724    fn is_origin_trusted_rejects_unknown() {
725        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
726        assert!(!cfg.is_origin_trusted("https://evil.com"));
727    }
728
729    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
730    #[test]
731    fn is_origin_trusted_glob_pattern() {
732        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
733            .trusted_origin("https://*.example.com");
734        assert!(cfg.is_origin_trusted("https://sub.example.com"));
735        assert!(!cfg.is_origin_trusted("https://other.com"));
736    }
737
738    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
739    #[test]
740    fn redirect_target_allows_relative_path() {
741        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
742        assert!(cfg.is_redirect_target_trusted("/dashboard"));
743        assert!(cfg.is_redirect_target_trusted("/callback?foo=bar"));
744    }
745
746    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
747    #[test]
748    fn redirect_target_allows_same_origin() {
749        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
750        assert!(cfg.is_redirect_target_trusted("https://myapp.com/verified"));
751    }
752
753    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
754    #[test]
755    fn redirect_target_allows_trusted_origin() {
756        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
757            .base_url("https://myapp.com")
758            .trusted_origin("https://trusted.com");
759        assert!(cfg.is_redirect_target_trusted("https://trusted.com/path"));
760    }
761
762    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
763    #[test]
764    fn redirect_target_rejects_untrusted_origin() {
765        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
766        assert!(!cfg.is_redirect_target_trusted("https://evil.com/phish"));
767    }
768
769    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
770    #[test]
771    fn redirect_target_rejects_protocol_relative() {
772        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
773        assert!(!cfg.is_redirect_target_trusted("//evil.com"));
774    }
775
776    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
777    #[test]
778    fn is_path_disabled_matches() {
779        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").disabled_path("/admin");
780        assert!(cfg.is_path_disabled("/admin"));
781        assert!(!cfg.is_path_disabled("/user"));
782    }
783
784    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
785    #[test]
786    fn validate_rejects_empty_secret() {
787        let cfg = AuthConfig::default();
788        assert!(cfg.validate().is_err());
789    }
790
791    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
792    #[test]
793    fn validate_rejects_short_secret() {
794        let cfg = AuthConfig::new("short");
795        assert!(cfg.validate().is_err());
796    }
797
798    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
799    #[test]
800    fn validate_accepts_valid_secret() {
801        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567");
802        assert!(cfg.validate().is_ok());
803    }
804
805    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
806    #[test]
807    fn session_config_defaults() {
808        let s = SessionConfig::default();
809        assert_eq!(s.expires_in, Duration::hours(24 * 7));
810        assert_eq!(s.update_age, Some(Duration::hours(24)));
811        assert!(!s.disable_session_refresh);
812        assert_eq!(s.cookie_name, "better-auth.session_token");
813        assert!(s.cookie_http_only);
814        assert_eq!(s.cookie_same_site, SameSite::Lax);
815    }
816
817    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
818    #[test]
819    fn jwt_config_defaults() {
820        let j = JwtConfig::default();
821        assert_eq!(j.expires_in, Duration::hours(24));
822        assert_eq!(j.algorithm, "HS256");
823    }
824
825    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
826    #[test]
827    fn password_config_defaults() {
828        let p = PasswordConfig::default();
829        assert!(!p.require_uppercase);
830    }
831
832    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
833    #[test]
834    fn same_site_display() {
835        assert_eq!(SameSite::Strict.to_string(), "Strict");
836        assert_eq!(SameSite::Lax.to_string(), "Lax");
837        assert_eq!(SameSite::None.to_string(), "None");
838    }
839
840    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
841    #[test]
842    fn cookie_cache_config_defaults() {
843        let c = CookieCacheConfig::default();
844        assert!(!c.enabled);
845        assert!((c.max_age - 300.0).abs() < f64::EPSILON);
846        assert_eq!(c.strategy, CookieCacheStrategy::Compact);
847    }
848
849    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
850    #[test]
851    fn account_config_defaults() {
852        let a = AccountConfig::default();
853        assert!(a.update_account_on_sign_in);
854        assert!(!a.encrypt_oauth_tokens);
855        assert!(a.account_linking.enabled);
856    }
857
858    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
859    #[test]
860    fn core_paths_error_page() {
861        let html = crate::error::page::error_page_html("TEST_ERROR");
862        assert!(html.contains("TEST_ERROR"));
863        assert!(html.contains("Ask AI"));
864        assert!(html.contains("<title>Error</title>"));
865    }
866
867    // Upstream reference: packages/better-auth/src/api/routes/error.ts :: sanitize function and /^[A-Za-z0-9_'-]+$/ whitelist.
868    #[test]
869    fn error_page_sanitizes_script_tag() {
870        let html = crate::error::page::error_page_html("<script>alert(1)</script>");
871        assert!(html.contains("UNKNOWN"));
872        assert!(!html.contains("<script>"));
873    }
874
875    // Upstream reference: packages/better-auth/src/api/routes/error.ts :: sanitize function and /^[A-Za-z0-9_'-]+$/ whitelist.
876    #[test]
877    fn error_page_allows_valid_codes() {
878        assert!(crate::error::page::error_page_html("SOME_ERROR-CODE").contains("SOME_ERROR-CODE"));
879        assert!(crate::error::page::error_page_html("it's").contains("it's"));
880    }
881
882    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
883    #[test]
884    fn session_builder_methods() {
885        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
886            .session_expires_in(Duration::hours(1))
887            .session_update_age(Duration::minutes(30))
888            .disable_session_refresh(true)
889            .session_fresh_age(Duration::minutes(5));
890
891        assert_eq!(cfg.session.expires_in, Duration::hours(1));
892        assert_eq!(cfg.session.update_age, Some(Duration::minutes(30)));
893        assert!(cfg.session.disable_session_refresh);
894        assert_eq!(cfg.session.fresh_age, Some(Duration::minutes(5)));
895    }
896
897    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
898    #[test]
899    fn session_cookie_cache_builder() {
900        let cache = CookieCacheConfig {
901            enabled: true,
902            max_age: 600.0,
903            strategy: CookieCacheStrategy::Jwt,
904            ..CookieCacheConfig::default()
905        };
906        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").session_cookie_cache(cache);
907
908        let cc = cfg.session.cookie_cache.as_ref();
909        assert!(cc.is_some());
910        let cc = cc.unwrap();
911        assert!(cc.enabled);
912        assert_eq!(cc.strategy, CookieCacheStrategy::Jwt);
913    }
914
915    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
916    #[test]
917    fn cross_sub_domain_cookies_builder() {
918        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
919            .cross_sub_domain_cookies(".example.com");
920        let csd = cfg.advanced.cross_sub_domain_cookies.as_ref();
921        assert!(csd.is_some());
922        assert_eq!(csd.unwrap().domain, ".example.com");
923    }
924
925    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
926    #[test]
927    fn advanced_database_defaults() {
928        let d = AdvancedDatabaseConfig::default();
929        assert_eq!(d.default_find_many_limit, 100);
930        assert!(!d.use_number_id);
931    }
932
933    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
934    #[test]
935    fn ip_address_config_defaults() {
936        let ip = IpAddressConfig::default();
937        assert_eq!(ip.headers, vec!["x-forwarded-for"]);
938        assert!(!ip.disable_ip_tracking);
939    }
940}
941// LCOV_EXCL_STOP