rustauth_core/cookies/
config.rs1use time::Duration;
2
3use crate::env::is_production_posture;
4use crate::error::RustAuthError;
5use crate::options::{CookieAttributesOverride, RustAuthOptions};
6
7use super::types::{
8 AuthCookie, AuthCookies, CookieOptions, DEFAULT_COOKIE_PREFIX, SECURE_COOKIE_PREFIX,
9};
10
11pub fn get_cookies(options: &RustAuthOptions) -> Result<AuthCookies, RustAuthError> {
12 let session_max_age = options
13 .session
14 .expires_in
15 .unwrap_or(Duration::days(7))
16 .whole_seconds() as u64;
17 let cache_max_age = options
18 .session
19 .cookie_cache
20 .max_age
21 .unwrap_or(Duration::minutes(5))
22 .whole_seconds() as u64;
23
24 Ok(AuthCookies {
25 session_token: create_auth_cookie(options, "session_token", Some(session_max_age))?,
26 session_data: create_auth_cookie(options, "session_data", Some(cache_max_age))?,
27 account_data: create_auth_cookie(options, "account_data", Some(cache_max_age))?,
28 dont_remember_token: create_auth_cookie(options, "dont_remember", None)?,
29 oauth_state: create_auth_cookie(options, "oauth_state", Some(60 * 10))?,
30 })
31}
32
33pub fn create_auth_cookie(
41 options: &RustAuthOptions,
42 name: &str,
43 max_age: Option<u64>,
44) -> Result<AuthCookie, RustAuthError> {
45 let secure = resolve_secure(options);
46 let secure_prefix = if secure { SECURE_COOKIE_PREFIX } else { "" };
47 let prefix = options
48 .advanced
49 .cookie_prefix
50 .as_deref()
51 .unwrap_or(DEFAULT_COOKIE_PREFIX);
52 let domain = resolve_domain(options)?;
53
54 Ok(AuthCookie {
55 name: format!("{secure_prefix}{prefix}.{name}"),
56 attributes: merge_cookie_attributes(
57 CookieOptions {
58 max_age,
59 expires: None,
60 domain,
61 path: Some("/".to_owned()),
62 secure: Some(secure),
63 http_only: Some(true),
64 same_site: Some("lax".to_owned()),
65 partitioned: None,
66 },
67 &options.advanced.default_cookie_attributes,
68 ),
69 })
70}
71
72fn resolve_secure(options: &RustAuthOptions) -> bool {
73 if let Some(secure) = options.advanced.use_secure_cookies {
74 return secure;
75 }
76 if let Some(base_url) = &options.base_url {
77 return base_url.starts_with("https://");
78 }
79 is_production_posture(options)
80}
81
82fn resolve_domain(options: &RustAuthOptions) -> Result<Option<String>, RustAuthError> {
83 let Some(config) = &options.advanced.cross_subdomain_cookies else {
84 return Ok(None);
85 };
86 if !config.enabled {
87 return Ok(None);
88 }
89 if let Some(domain) = &config.domain {
90 return Ok(Some(domain.clone()));
91 }
92 let Some(base_url) = &options.base_url else {
93 return Err(RustAuthError::Cookie(
94 "base_url is required when cross-subdomain cookies are enabled".to_owned(),
95 ));
96 };
97 host_from_url(base_url)
98 .map(Some)
99 .ok_or_else(|| RustAuthError::Cookie("could not resolve cookie domain".to_owned()))
100}
101
102fn host_from_url(url: &str) -> Option<String> {
103 let (_, rest) = url.split_once("://")?;
104 let host = rest.split('/').next().unwrap_or(rest);
105 let host = host.split(':').next().unwrap_or(host);
106 (!host.is_empty()).then(|| host.to_owned())
107}
108
109fn merge_cookie_attributes(
110 mut base: CookieOptions,
111 override_attrs: &CookieAttributesOverride,
112) -> CookieOptions {
113 if override_attrs.domain.is_some() {
114 base.domain.clone_from(&override_attrs.domain);
115 }
116 if override_attrs.path.is_some() {
117 base.path.clone_from(&override_attrs.path);
118 }
119 if override_attrs.secure.is_some() {
120 base.secure = override_attrs.secure;
121 }
122 if override_attrs.http_only.is_some() {
123 base.http_only = override_attrs.http_only;
124 }
125 if override_attrs.same_site.is_some() {
126 base.same_site.clone_from(&override_attrs.same_site);
127 }
128 if override_attrs.max_age.is_some() {
129 base.max_age = override_attrs.max_age.map(|d| d.whole_seconds() as u64);
130 }
131 if override_attrs.partitioned.is_some() {
132 base.partitioned = override_attrs.partitioned;
133 }
134 base
135}
136
137pub(super) fn merge_options(mut base: CookieOptions, overrides: CookieOptions) -> CookieOptions {
138 if overrides.max_age.is_some() {
139 base.max_age = overrides.max_age;
140 }
141 if overrides.expires.is_some() {
142 base.expires = overrides.expires;
143 }
144 if overrides.domain.is_some() {
145 base.domain = overrides.domain;
146 }
147 if overrides.path.is_some() {
148 base.path = overrides.path;
149 }
150 if overrides.secure.is_some() {
151 base.secure = overrides.secure;
152 }
153 if overrides.http_only.is_some() {
154 base.http_only = overrides.http_only;
155 }
156 if overrides.same_site.is_some() {
157 base.same_site = overrides.same_site;
158 }
159 if overrides.partitioned.is_some() {
160 base.partitioned = overrides.partitioned;
161 }
162 base
163}