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}