Skip to main content

rustauth_core/cookies/
session.rs

1use crate::error::RustAuthError;
2
3use super::chunked::ChunkedCookieStore;
4use super::config::merge_options;
5use super::parse::parse_cookies;
6use super::signing::sign_cookie_value;
7use super::types::{
8    strip_secure_cookie_prefix, AuthCookie, AuthCookies, Cookie, SessionCookieOptions,
9    DEFAULT_COOKIE_PREFIX, SECURE_COOKIE_PREFIX,
10};
11
12pub fn get_session_cookie(
13    cookie_header: &str,
14    cookie_prefix: Option<&str>,
15    cookie_name: Option<&str>,
16    secure: bool,
17) -> Option<String> {
18    let prefix = cookie_prefix.unwrap_or(DEFAULT_COOKIE_PREFIX);
19    let name = cookie_name.unwrap_or("session_token");
20    let full_name = format!("{prefix}.{name}");
21    let legacy_name = format!("{prefix}-{name}");
22    let cookies = parse_cookies(cookie_header);
23
24    // In secure-cookie mode the server only ever sets the `__Secure-` prefixed
25    // name, so we accept the secure name (and its legacy `-` separator alias)
26    // exclusively. Reading the unprefixed cookie there would let a sibling app
27    // or subdomain that can write parent-domain cookies shadow the real secure
28    // session. In plain mode the secure names cannot legitimately exist.
29    let candidates = if secure {
30        [
31            format!("{SECURE_COOKIE_PREFIX}{full_name}"),
32            format!("{SECURE_COOKIE_PREFIX}{legacy_name}"),
33        ]
34    } else {
35        [full_name, legacy_name]
36    };
37
38    candidates
39        .iter()
40        .find_map(|candidate| cookies.get(candidate))
41        .cloned()
42}
43
44pub fn set_session_cookie(
45    auth_cookies: &AuthCookies,
46    secret: &str,
47    token: &str,
48    options: SessionCookieOptions,
49) -> Result<Vec<Cookie>, RustAuthError> {
50    let mut attributes = merge_options(
51        auth_cookies.session_token.attributes.clone(),
52        options.overrides,
53    );
54    if options.dont_remember {
55        attributes.max_age = None;
56    }
57
58    let mut cookies = vec![Cookie {
59        name: auth_cookies.session_token.name.clone(),
60        value: sign_cookie_value(token, secret)?,
61        attributes,
62    }];
63
64    if options.dont_remember {
65        cookies.push(Cookie {
66            name: auth_cookies.dont_remember_token.name.clone(),
67            value: sign_cookie_value("true", secret)?,
68            attributes: auth_cookies.dont_remember_token.attributes.clone(),
69        });
70    }
71
72    Ok(cookies)
73}
74
75pub fn expire_cookie(cookie: &AuthCookie) -> Cookie {
76    let mut attributes = cookie.attributes.clone();
77    attributes.max_age = Some(0);
78    Cookie {
79        name: cookie.name.clone(),
80        value: String::new(),
81        attributes,
82    }
83}
84
85pub fn delete_session_cookie(
86    auth_cookies: &AuthCookies,
87    cookie_header: &str,
88    skip_dont_remember: bool,
89) -> Vec<Cookie> {
90    let mut expired = vec![
91        expire_cookie(&auth_cookies.session_token),
92        expire_cookie(&auth_cookies.session_data),
93        expire_cookie(&auth_cookies.account_data),
94    ];
95    // In secure mode, also expire the unprefixed fallback name so a shadow
96    // cookie planted by a sibling app or subdomain cannot linger and keep
97    // forcing anonymous responses after sign-out or an invalid-cookie reset.
98    expired.extend(expire_unprefixed_alias(&auth_cookies.session_token));
99    expired.extend(clean_chunked_cookie(
100        &auth_cookies.session_data,
101        cookie_header,
102    ));
103    expired.extend(clean_chunked_cookie(
104        &auth_cookies.account_data,
105        cookie_header,
106    ));
107    if !skip_dont_remember {
108        expired.push(expire_cookie(&auth_cookies.dont_remember_token));
109    }
110    expired
111}
112
113fn clean_chunked_cookie(cookie: &AuthCookie, cookie_header: &str) -> Vec<Cookie> {
114    ChunkedCookieStore::new(
115        cookie.name.clone(),
116        cookie.attributes.clone(),
117        cookie_header,
118    )
119    .clean()
120}
121
122/// Expire the unprefixed alias of a secure-prefixed cookie.
123///
124/// Returns `None` when the cookie is not secure-prefixed (no shadow alias can
125/// exist), otherwise an expired cookie targeting the stripped, unprefixed name.
126fn expire_unprefixed_alias(cookie: &AuthCookie) -> Option<Cookie> {
127    let stripped = strip_secure_cookie_prefix(&cookie.name);
128    (stripped != cookie.name).then(|| {
129        let mut expired = expire_cookie(cookie);
130        expired.name = stripped.to_owned();
131        expired
132    })
133}