codoseo_web/auth/
session.rs1use axum::http::{HeaderMap, HeaderValue, header};
7use base64::Engine;
8use base64::engine::general_purpose::URL_SAFE_NO_PAD;
9use sha2::{Digest, Sha256};
10use uuid::Uuid;
11
12use crate::error::AppError;
13use crate::state::AppState;
14
15pub const SESSION_COOKIE: &str = "codoseo_session";
16pub const SESSION_TTL: time::Duration = time::Duration::days(30);
17
18pub fn random_token() -> String {
20 let mut bytes = [0u8; 32];
21 rand::fill(&mut bytes);
22 URL_SAFE_NO_PAD.encode(bytes)
23}
24
25pub fn hash(token: &str) -> Vec<u8> {
27 Sha256::digest(token.as_bytes()).to_vec()
28}
29
30pub fn cookie(headers: &HeaderMap, name: &str) -> Option<String> {
32 headers
33 .get_all(header::COOKIE)
34 .iter()
35 .filter_map(|v| v.to_str().ok())
36 .flat_map(|v| v.split(';'))
37 .filter_map(|pair| pair.trim().split_once('='))
38 .find(|(k, _)| *k == name)
39 .map(|(_, v)| v.to_owned())
40}
41
42pub fn set_cookie(name: &str, value: &str, max_age_secs: i64, secure: bool) -> HeaderValue {
44 let secure = if secure { "; Secure" } else { "" };
45 HeaderValue::from_str(&format!(
46 "{name}={value}; Path=/; Max-Age={max_age_secs}; HttpOnly; SameSite=Lax{secure}"
47 ))
48 .expect("cookie values are base64 or empty")
49}
50
51pub async fn start(state: &AppState, account_id: Uuid) -> Result<HeaderValue, AppError> {
53 let token = random_token();
54 codoseo_store::auth::create_session(&state.pool, account_id, &hash(&token), SESSION_TTL)
55 .await?;
56 codoseo_store::accounts::touch_login(&state.pool, account_id).await?;
57 Ok(set_cookie(
58 SESSION_COOKIE,
59 &token,
60 SESSION_TTL.whole_seconds(),
61 state.config.secure_cookies(),
62 ))
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn tokens_are_random_and_hash_to_32_bytes() {
71 let a = random_token();
72 let b = random_token();
73 assert_ne!(a, b);
74 assert_eq!(a.len(), 43);
75 assert_eq!(hash(&a).len(), 32);
76 assert_eq!(hash(&a), hash(&a));
77 }
78
79 #[test]
80 fn reads_a_cookie_among_others() {
81 let mut h = HeaderMap::new();
82 h.insert(
83 header::COOKIE,
84 HeaderValue::from_static("theme=dark; codoseo_session=abc; x=1"),
85 );
86 assert_eq!(cookie(&h, SESSION_COOKIE).as_deref(), Some("abc"));
87 assert_eq!(cookie(&h, "missing"), None);
88 }
89
90 #[test]
91 fn cookie_flags() {
92 let v = set_cookie("s", "v", 60, true);
93 let s = v.to_str().unwrap();
94 assert!(s.contains("HttpOnly") && s.contains("SameSite=Lax") && s.contains("Secure"));
95 assert!(
96 !set_cookie("s", "v", 60, false)
97 .to_str()
98 .unwrap()
99 .contains("Secure")
100 );
101 }
102}