Skip to main content

rustauth_core/cookies/
config.rs

1use 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
33/// Build a single auth cookie definition using the same name prefixing and
34/// attribute merge policy as [`get_cookies`].
35///
36/// Plugins should route their own security-sensitive cookies (for example the
37/// passkey challenge cookie) through this helper so they inherit the configured
38/// `cookie_prefix`, secure-name prefix, cross-subdomain `domain`, and
39/// `default_cookie_attributes` instead of using a raw, unnamespaced cookie name.
40pub 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}