pub const SESSION_COOKIE_NAME: &str = "pc_web_session";
#[must_use]
pub fn set_session_cookie_header(token: &str, domain: &str) -> String {
let max_age_secs = crate::SESSION_TTL_MS / 1_000;
format!(
"{SESSION_COOKIE_NAME}={token}; HttpOnly; Secure; SameSite=Lax; Path=/; \
Domain={domain}; Max-Age={max_age_secs}"
)
}
#[must_use]
pub fn clear_session_cookie_header(domain: &str) -> String {
format!(
"{SESSION_COOKIE_NAME}=; HttpOnly; Secure; SameSite=Lax; Path=/; Domain={domain}; \
Max-Age=0"
)
}
#[must_use]
pub fn extract_session_cookie(raw_cookie_header: &str) -> Option<String> {
raw_cookie_header.split(';').map(str::trim).find_map(|kv| {
let (name, value) = kv.split_once('=')?;
(name == SESSION_COOKIE_NAME).then(|| value.to_owned())
})
}
pub const GRANT_COOKIE_NAME: &str = "pc_web_grant";
#[must_use]
pub fn set_grant_cookie_header(
grant: &str,
domain: &str,
family_expires_ms: u64,
now_ms: u64,
) -> String {
let max_age_secs = family_expires_ms.saturating_sub(now_ms) / 1_000;
format!(
"{GRANT_COOKIE_NAME}={grant}; HttpOnly; Secure; SameSite=Lax; Path=/; \
Domain={domain}; Max-Age={max_age_secs}"
)
}
#[must_use]
pub fn clear_grant_cookie_header(domain: &str) -> String {
format!(
"{GRANT_COOKIE_NAME}=; HttpOnly; Secure; SameSite=Lax; Path=/; Domain={domain}; \
Max-Age=0"
)
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
#[test]
fn set_then_extract_round_trips_the_token() {
let header = set_session_cookie_header("tok-123", "explore.polychrome.test");
assert!(header.starts_with("pc_web_session=tok-123;"));
assert!(header.contains("Domain=explore.polychrome.test"));
assert!(header.contains("Max-Age=900"));
let cookie_value_line = header.split(';').next().unwrap();
assert_eq!(
extract_session_cookie(cookie_value_line),
Some("tok-123".to_owned())
);
}
#[test]
fn extract_finds_the_named_cookie_among_several() {
let raw = "other=ignored; pc_web_session=the-token; another=also-ignored";
assert_eq!(extract_session_cookie(raw), Some("the-token".to_owned()));
}
#[test]
fn extract_returns_none_when_absent() {
assert_eq!(extract_session_cookie("other=ignored"), None);
assert_eq!(extract_session_cookie(""), None);
}
#[test]
fn clear_header_expires_immediately_with_the_same_scope() {
let header = clear_session_cookie_header("explore.polychrome.test");
assert!(header.starts_with("pc_web_session=;"));
assert!(header.contains("Domain=explore.polychrome.test"));
assert!(header.contains("Max-Age=0"));
}
#[test]
fn grant_cookie_max_age_tracks_the_family_deadline_not_the_token_ttl() {
let now_ms = 1_700_000_000_000_u64;
let family_expires_ms = now_ms + 90 * 24 * 60 * 60 * 1000;
let header = set_grant_cookie_header(
"grant-abc",
"explore.polychrome.test",
family_expires_ms,
now_ms,
);
assert!(header.starts_with("pc_web_grant=grant-abc;"));
assert!(header.contains("HttpOnly"));
assert!(header.contains("Secure"));
assert!(header.contains("SameSite=Lax"));
assert!(header.contains("Path=/"));
assert!(header.contains("Domain=explore.polychrome.test"));
assert!(header.contains("Max-Age=7776000"));
let cookie_value_line = header.split(';').next().unwrap();
let (name, value) = cookie_value_line.split_once('=').unwrap();
assert_eq!(name, GRANT_COOKIE_NAME);
assert_eq!(value, "grant-abc");
}
#[test]
fn grant_cookie_max_age_floors_at_zero_rather_than_underflowing() {
let now_ms = 1_700_000_000_000_u64;
let header =
set_grant_cookie_header("grant-abc", "explore.polychrome.test", now_ms - 1, now_ms);
assert!(header.contains("Max-Age=0"));
}
#[test]
fn clear_grant_header_expires_immediately_with_the_same_scope() {
let header = clear_grant_cookie_header("explore.polychrome.test");
assert!(header.starts_with("pc_web_grant=;"));
assert!(header.contains("Domain=explore.polychrome.test"));
assert!(header.contains("Max-Age=0"));
}
}