Skip to main content

alibi_core/config/
advanced.rs

1use crate::AuthError;
2use crate::config::{DatabaseIdStrategy, HashMap};
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum SameSite {
5    Strict,
6    Lax,
7    None,
8}
9
10impl std::fmt::Display for SameSite {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        match self {
13            Self::Strict => f.write_str("Strict"),
14            Self::Lax => f.write_str("Lax"),
15            Self::None => f.write_str("None"),
16        }
17    }
18}
19
20/// Advanced configuration options (mirrors TS `advanced` block).
21#[derive(Debug, Clone, Default)]
22pub struct AdvancedConfig {
23    /// Trust forwarded host/protocol for URL resolution. Enable only behind a
24    /// proxy that replaces client-supplied forwarding headers. Defaults to false.
25    pub trust_forwarded_host: bool,
26
27    /// IP address extraction configuration.
28    pub ip_address: IpAddressConfig,
29
30    /// Explicit CSRF override. `None` preserves the pinned compatibility behavior
31    /// where disabling all origin checks also disables first-login CSRF checks.
32    pub disable_csrf_check: Option<bool>,
33
34    /// If `true`, callback / redirect target origin validation is skipped.
35    ///
36    /// This mirrors Better Auth TS `advanced.disableOriginCheck`.
37    /// Request-origin validation is also skipped. First-login Fetch Metadata
38    /// checks remain enabled when `disable_csrf_check` is explicitly `Some(false)`.
39    pub disable_origin_check: bool,
40
41    /// Skip origin validation for a literal path and its descendants. This does
42    /// not disable first-login cross-site navigation protection.
43    pub disable_origin_check_paths: Vec<String>,
44
45    /// Admit trailing slashes while resolving the original registered endpoint.
46    pub skip_trailing_slashes: bool,
47
48    /// Select the secure cookie name prefix and initial Secure attribute.
49    /// None uses the configured base URL protocol. Attribute overrides do not
50    /// change this choice; the serializer enforces Secure for reserved prefixes.
51    pub use_secure_cookies: Option<bool>,
52
53    /// Cross-subdomain cookie sharing configuration.
54    pub cross_sub_domain_cookies: Option<CrossSubDomainConfig>,
55
56    /// Per-cookie-name overrides (name, attributes, prefix).
57    ///
58    /// Keys are the *logical* cookie names (e.g. `"session_token"`,
59    /// `"csrf_token"`). Values specify the attributes to override.
60    pub cookies: HashMap<String, CookieOverride>,
61
62    /// Default cookie attributes applied to *every* cookie the library sets
63    /// (individual overrides in `cookies` take precedence).
64    pub default_cookie_attributes: CookieAttributes,
65
66    /// Optional prefix prepended to every cookie name (e.g. `"myapp"` →
67    /// `"myapp.session_token"`).
68    pub cookie_prefix: Option<String>,
69
70    /// Database-related advanced options.
71    pub database: AdvancedDatabaseConfig,
72}
73
74/// IP-address extraction configuration.
75#[derive(Debug, Clone)]
76pub struct IpAddressConfig {
77    /// Ordered list of headers to check for the client IP.
78    /// Defaults to `["x-forwarded-for"]`. Header names are case insensitive.
79    pub headers: Vec<String>,
80
81    /// IP addresses or CIDRs removed from the right of a forwarded chain.
82    /// Invalid entries are ignored. Without valid entries, only a single
83    /// address is admitted. Deployments must prevent clients bypassing the
84    /// proxy and supplying their own trusted forwarding headers.
85    pub trusted_proxies: Vec<String>,
86
87    /// IPv6 grouping prefix; defaults to 64. Fractional values are floored,
88    /// negative values become zero, and values at least 128 or NaN preserve
89    /// the full address. IPv4-mapped addresses use IPv4 grouping.
90    pub ipv6_subnet: f64,
91
92    /// Fall back to localhost when no configured header resolves an address.
93    /// Defaults to true for `NODE_ENV` dev/development/test or a truthy TEST
94    /// environment flag. Applications may configure this explicitly.
95    pub localhost_fallback: bool,
96
97    /// If `true`, IP tracking is entirely disabled (no IP stored in sessions).
98    pub disable_ip_tracking: bool,
99}
100
101/// Configuration for sharing cookies across sub-domains.
102#[derive(Debug, Clone, Default)]
103pub struct CrossSubDomainConfig {
104    /// Explicit cookie domain (e.g. `".example.com"`). An empty value infers
105    /// the hostname of the configured or request-resolved base URL, without a port.
106    pub domain: String,
107}
108
109/// Overridable cookie attributes.
110#[derive(Debug, Clone, Default)]
111pub struct CookieAttributes {
112    /// Override `Secure` flag.
113    pub secure: Option<bool>,
114    /// Override `HttpOnly` flag.
115    pub http_only: Option<bool>,
116    /// Override `SameSite` policy.
117    pub same_site: Option<SameSite>,
118    /// Override `Path`.
119    pub path: Option<String>,
120    /// Override `Max-Age` (seconds).
121    pub max_age: Option<f64>,
122    /// Explicit expiry, checked at emission against the published 400-day limit.
123    pub expires: Option<chrono::DateTime<chrono::Utc>>,
124    /// Emit the published `Partitioned` attribute after `SameSite`.
125    pub partitioned: Option<bool>,
126    /// Override cookie `Domain`.
127    pub domain: Option<String>,
128}
129
130/// Per-cookie override entry.
131#[derive(Debug, Clone, Default)]
132pub struct CookieOverride {
133    /// Custom name to use instead of the logical name.
134    pub name: Option<String>,
135    /// Attribute overrides for this cookie.
136    pub attributes: CookieAttributes,
137}
138
139/// Physical storage policy for the bundled two-factor model.
140#[derive(Debug, Clone)]
141pub struct TwoFactorDatabaseConfig {
142    pub table_name: String,
143    /// Canonical ``snake_case`` column names mapped to application-owned columns.
144    pub columns: std::collections::HashMap<String, String>,
145}
146
147/// Database-related advanced options.
148#[derive(Debug, Clone)]
149pub struct AdvancedDatabaseConfig {
150    /// Default `LIMIT` for "find many" queries.
151    pub default_find_many_limit: usize,
152
153    /// PostgreSQL namespace for every auth relation; does not change ``search_path``.
154    pub schema_name: Option<String>,
155
156    /// Optional physical table and column mapping for two-factor credentials.
157    pub two_factor: Option<TwoFactorDatabaseConfig>,
158
159    /// Use persistent serial identifiers when no explicit generation strategy is set.
160    /// Numeric invitation IDs require email verification by default.
161    pub use_number_id: bool,
162
163    /// Override the model default for every adapter-created identifier.
164    pub generate_id: Option<DatabaseIdStrategy>,
165}
166
167impl Default for IpAddressConfig {
168    fn default() -> Self {
169        Self {
170            headers: vec!["x-forwarded-for".to_owned()],
171            trusted_proxies: Vec::new(),
172            ipv6_subnet: 64.0,
173            localhost_fallback: matches!(
174                std::env::var("NODE_ENV").as_deref(),
175                Ok("dev" | "development" | "test")
176            ) || std::env::var("TEST")
177                .is_ok_and(|value| !matches!(value.as_str(), "" | "false")),
178            disable_ip_tracking: false,
179        }
180    }
181}
182
183impl Default for AdvancedDatabaseConfig {
184    fn default() -> Self {
185        Self {
186            default_find_many_limit: 100,
187            schema_name: None,
188            two_factor: None,
189            use_number_id: false,
190            generate_id: None,
191        }
192    }
193}
194
195impl TwoFactorDatabaseConfig {
196    /// DDL used to migrate the bundled factor model to its configured physical layout.
197    pub fn migration_statements(&self) -> crate::AuthResult<Vec<String>> {
198        const COLUMNS: &[&str] = &[
199            "id",
200            "secret",
201            "backup_codes",
202            "user_id",
203            "verified",
204            "failed_verification_count",
205            "locked_until",
206            "created_at",
207            "updated_at",
208        ];
209        if self.table_name.is_empty()
210            || self
211                .columns
212                .iter()
213                .any(|(name, column)| !COLUMNS.contains(&name.as_str()) || column.is_empty())
214        {
215            return Err(AuthError::config("Invalid two-factor storage mapping"));
216        }
217        let quote = |name: &str| format!("\"{}\"", name.replace('"', "\"\""));
218        let table = quote(&self.table_name);
219        let mut sql = Vec::new();
220        if self.table_name != "two_factor" {
221            sql.push(format!("ALTER TABLE \"two_factor\" RENAME TO {table}"));
222        }
223        for name in COLUMNS {
224            if let Some(column) = self
225                .columns
226                .get(*name)
227                .filter(|column| column.as_str() != *name)
228            {
229                sql.push(format!(
230                    "ALTER TABLE {table} RENAME COLUMN {} TO {}",
231                    quote(name),
232                    quote(column)
233                ));
234            }
235        }
236        Ok(sql)
237    }
238}