1use crate::email::EmailProvider;
2use crate::error::AuthError;
3use chrono::Duration;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7pub mod core_paths {
13 pub const OK: &str = "/ok";
14 pub const ERROR: &str = "/error";
15 pub const HEALTH: &str = "/health";
16 pub const OPENAPI_SPEC: &str = "/reference/openapi.json";
17 pub const UPDATE_USER: &str = "/update-user";
18 pub const DELETE_USER: &str = "/delete-user";
19 pub const CHANGE_EMAIL: &str = "/change-email";
20 pub const DELETE_USER_CALLBACK: &str = "/delete-user/callback";
21}
22
23#[derive(Clone)]
25pub struct AuthConfig {
26 pub secret: String,
28
29 pub app_name: String,
33
34 pub base_url: String,
36
37 pub base_path: String,
45
46 pub trusted_origins: Vec<String>,
52
53 pub disabled_paths: Vec<String>,
58 pub session: SessionConfig,
60
61 pub jwt: JwtConfig,
63
64 pub password: PasswordConfig,
66
67 pub account: AccountConfig,
69
70 pub email_provider: Option<Arc<dyn EmailProvider>>,
72
73 pub advanced: AdvancedConfig,
75}
76
77#[derive(Debug, Clone)]
79pub struct AccountConfig {
80 pub update_account_on_sign_in: bool,
82 pub account_linking: AccountLinkingConfig,
84 pub encrypt_oauth_tokens: bool,
86}
87
88#[derive(Debug, Clone)]
90pub struct AccountLinkingConfig {
91 pub enabled: bool,
93 pub trusted_providers: Vec<String>,
95 pub allow_different_emails: bool,
97 pub allow_unlinking_all: bool,
99 pub update_user_info_on_link: bool,
101}
102
103#[derive(Debug, Clone)]
105pub struct SessionConfig {
106 pub expires_in: Duration,
108
109 pub update_age: Option<Duration>,
115
116 pub disable_session_refresh: bool,
118
119 pub fresh_age: Option<Duration>,
122
123 pub cookie_name: String,
125
126 pub cookie_secure: bool,
128 pub cookie_http_only: bool,
129 pub cookie_same_site: SameSite,
130
131 pub cookie_cache: Option<CookieCacheConfig>,
136}
137
138#[derive(Debug, Clone)]
140pub struct JwtConfig {
141 pub expires_in: Duration,
143
144 pub algorithm: String,
146
147 pub issuer: Option<String>,
149
150 pub audience: Option<String>,
152}
153
154#[derive(Debug, Clone)]
156pub struct PasswordConfig {
157 pub min_length: usize,
159
160 pub require_uppercase: bool,
162
163 pub require_lowercase: bool,
165
166 pub require_numbers: bool,
168
169 pub require_special: bool,
171
172 pub argon2_config: Argon2Config,
174}
175
176#[derive(Debug, Clone)]
178pub struct Argon2Config {
179 pub memory_cost: u32,
180 pub time_cost: u32,
181 pub parallelism: u32,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub enum SameSite {
186 Strict,
187 Lax,
188 None,
189}
190
191#[derive(Debug, Clone)]
196pub struct CookieCacheConfig {
197 pub enabled: bool,
199
200 pub max_age: Duration,
204
205 pub strategy: CookieCacheStrategy,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum CookieCacheStrategy {
212 Compact,
214 Jwt,
216 Jwe,
218}
219
220impl Default for CookieCacheConfig {
221 fn default() -> Self {
222 Self {
223 enabled: false,
224 max_age: Duration::minutes(5),
225 strategy: CookieCacheStrategy::Compact,
226 }
227 }
228}
229
230impl Default for AccountConfig {
231 fn default() -> Self {
232 Self {
233 update_account_on_sign_in: true,
234 account_linking: AccountLinkingConfig::default(),
235 encrypt_oauth_tokens: false,
236 }
237 }
238}
239
240impl Default for AccountLinkingConfig {
241 fn default() -> Self {
242 Self {
243 enabled: true,
244 trusted_providers: Vec::new(),
245 allow_different_emails: false,
246 allow_unlinking_all: false,
247 update_user_info_on_link: false,
248 }
249 }
250}
251
252impl std::fmt::Display for SameSite {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 match self {
255 SameSite::Strict => f.write_str("Strict"),
256 SameSite::Lax => f.write_str("Lax"),
257 SameSite::None => f.write_str("None"),
258 }
259 }
260}
261
262#[derive(Debug, Clone, Default)]
266pub struct AdvancedConfig {
267 pub ip_address: IpAddressConfig,
269
270 pub disable_csrf_check: bool,
272
273 pub disable_origin_check: bool,
275
276 pub cross_sub_domain_cookies: Option<CrossSubDomainConfig>,
278
279 pub cookies: HashMap<String, CookieOverride>,
284
285 pub default_cookie_attributes: CookieAttributes,
288
289 pub cookie_prefix: Option<String>,
292
293 pub database: AdvancedDatabaseConfig,
295
296 pub trusted_proxy_headers: Vec<String>,
299}
300
301#[derive(Debug, Clone)]
303pub struct IpAddressConfig {
304 pub headers: Vec<String>,
307
308 pub disable_ip_tracking: bool,
310}
311
312#[derive(Debug, Clone)]
314pub struct CrossSubDomainConfig {
315 pub domain: String,
317}
318
319#[derive(Debug, Clone, Default)]
321pub struct CookieAttributes {
322 pub secure: Option<bool>,
324 pub http_only: Option<bool>,
326 pub same_site: Option<SameSite>,
328 pub path: Option<String>,
330 pub max_age: Option<i64>,
332 pub domain: Option<String>,
334}
335
336#[derive(Debug, Clone, Default)]
338pub struct CookieOverride {
339 pub name: Option<String>,
341 pub attributes: CookieAttributes,
343}
344
345#[derive(Debug, Clone)]
347pub struct AdvancedDatabaseConfig {
348 pub default_find_many_limit: usize,
350
351 pub use_number_id: bool,
354}
355impl Default for AuthConfig {
356 fn default() -> Self {
357 Self {
358 secret: String::new(),
359 app_name: "Better Auth".to_string(),
360 base_url: "http://localhost:3000".to_string(),
361 base_path: "/api/auth".to_string(),
362 trusted_origins: Vec::new(),
363 disabled_paths: Vec::new(),
364 session: SessionConfig::default(),
365 jwt: JwtConfig::default(),
366 password: PasswordConfig::default(),
367 account: AccountConfig::default(),
368 email_provider: None,
369 advanced: AdvancedConfig::default(),
370 }
371 }
372}
373
374impl Default for SessionConfig {
375 fn default() -> Self {
376 Self {
377 expires_in: Duration::hours(24 * 7), update_age: Some(Duration::hours(24)), disable_session_refresh: false,
380 fresh_age: None,
381 cookie_name: "better-auth.session-token".to_string(),
382 cookie_secure: true,
383 cookie_http_only: true,
384 cookie_same_site: SameSite::Lax,
385 cookie_cache: None,
386 }
387 }
388}
389
390impl Default for IpAddressConfig {
391 fn default() -> Self {
392 Self {
393 headers: vec!["x-forwarded-for".to_string(), "x-real-ip".to_string()],
394 disable_ip_tracking: false,
395 }
396 }
397}
398
399impl Default for AdvancedDatabaseConfig {
400 fn default() -> Self {
401 Self {
402 default_find_many_limit: 100,
403 use_number_id: false,
404 }
405 }
406}
407
408impl Default for JwtConfig {
409 fn default() -> Self {
410 Self {
411 expires_in: Duration::hours(24), algorithm: "HS256".to_string(),
413 issuer: None,
414 audience: None,
415 }
416 }
417}
418
419impl Default for PasswordConfig {
420 fn default() -> Self {
421 Self {
422 min_length: 8,
423 require_uppercase: false,
424 require_lowercase: false,
425 require_numbers: false,
426 require_special: false,
427 argon2_config: Argon2Config::default(),
428 }
429 }
430}
431
432impl Default for Argon2Config {
433 fn default() -> Self {
434 Self {
435 memory_cost: 4096, time_cost: 3, parallelism: 1, }
439 }
440}
441
442impl AuthConfig {
443 pub fn new(secret: impl Into<String>) -> Self {
444 Self {
445 secret: secret.into(),
446 ..Default::default()
447 }
448 }
449
450 pub fn app_name(mut self, name: impl Into<String>) -> Self {
452 self.app_name = name.into();
453 self
454 }
455
456 pub fn base_url(mut self, url: impl Into<String>) -> Self {
458 self.base_url = url.into();
459 self
460 }
461
462 pub fn account(mut self, account: AccountConfig) -> Self {
463 self.account = account;
464 self
465 }
466
467 pub fn base_path(mut self, path: impl Into<String>) -> Self {
469 self.base_path = path.into();
470 self
471 }
472
473 pub fn trusted_origin(mut self, origin: impl Into<String>) -> Self {
475 self.trusted_origins.push(origin.into());
476 self
477 }
478
479 pub fn trusted_origins(mut self, origins: Vec<String>) -> Self {
481 self.trusted_origins = origins;
482 self
483 }
484
485 pub fn disabled_path(mut self, path: impl Into<String>) -> Self {
487 self.disabled_paths.push(path.into());
488 self
489 }
490
491 pub fn disabled_paths(mut self, paths: Vec<String>) -> Self {
493 self.disabled_paths = paths;
494 self
495 }
496
497 pub fn session_expires_in(mut self, duration: Duration) -> Self {
499 self.session.expires_in = duration;
500 self
501 }
502
503 pub fn session_update_age(mut self, duration: Duration) -> Self {
504 self.session.update_age = Some(duration);
505 self
506 }
507
508 pub fn disable_session_refresh(mut self, disabled: bool) -> Self {
509 self.session.disable_session_refresh = disabled;
510 self
511 }
512
513 pub fn session_fresh_age(mut self, duration: Duration) -> Self {
514 self.session.fresh_age = Some(duration);
515 self
516 }
517
518 pub fn session_cookie_cache(mut self, config: CookieCacheConfig) -> Self {
520 self.session.cookie_cache = Some(config);
521 self
522 }
523
524 pub fn jwt_expires_in(mut self, duration: Duration) -> Self {
526 self.jwt.expires_in = duration;
527 self
528 }
529
530 pub fn password_min_length(mut self, length: usize) -> Self {
532 self.password.min_length = length;
533 self
534 }
535
536 pub fn advanced(mut self, advanced: AdvancedConfig) -> Self {
537 self.advanced = advanced;
538 self
539 }
540
541 pub fn cookie_prefix(mut self, prefix: impl Into<String>) -> Self {
542 self.advanced.cookie_prefix = Some(prefix.into());
543 self
544 }
545
546 pub fn disable_csrf_check(mut self, disabled: bool) -> Self {
547 self.advanced.disable_csrf_check = disabled;
548 self
549 }
550
551 pub fn cross_sub_domain_cookies(mut self, domain: impl Into<String>) -> Self {
552 self.advanced.cross_sub_domain_cookies = Some(CrossSubDomainConfig {
553 domain: domain.into(),
554 });
555 self
556 }
557
558 pub fn is_origin_trusted(&self, origin: &str) -> bool {
568 if let Some(base_origin) = extract_origin(&self.base_url)
570 && origin == base_origin
571 {
572 return true;
573 }
574 self.trusted_origins.iter().any(|pattern| {
576 let pattern_origin = extract_origin(pattern).unwrap_or_default();
577 glob_match::glob_match(&pattern_origin, origin)
578 })
579 }
580
581 pub fn is_path_disabled(&self, path: &str) -> bool {
583 self.disabled_paths.iter().any(|disabled| disabled == path)
584 }
585 pub fn validate(&self) -> Result<(), AuthError> {
586 if self.secret.is_empty() {
587 return Err(AuthError::config("Secret key cannot be empty"));
588 }
589
590 if self.secret.len() < 32 {
591 return Err(AuthError::config(
592 "Secret key must be at least 32 characters",
593 ));
594 }
595
596 Ok(())
597 }
598}
599
600pub fn extract_origin(url: &str) -> Option<String> {
607 let scheme_end = url.find("://")?;
608 let rest = &url[scheme_end + 3..];
609 let host_end = rest.find('/').unwrap_or(rest.len());
610 let origin = format!("{}{}", &url[..scheme_end + 3], &rest[..host_end]);
611 Some(origin)
612}