yggdrasil/auth/
session.rs1#[cfg(feature = "server")]
8use chrono::{DateTime, Duration, Utc};
9#[cfg(feature = "server")]
10use uuid::Uuid;
11
12#[cfg(feature = "server")]
13pub fn generate_token() -> String {
15 Uuid::new_v4().to_string()
16}
17
18#[cfg(feature = "server")]
19pub fn hash_token(token: &str) -> String {
21 crate::utils::server::sha256_hex(token)
22}
23
24#[cfg(feature = "server")]
25pub const SESSION_MAX_AGE_SECS: i32 = 30 * 24 * 60 * 60;
28
29#[cfg(feature = "server")]
30pub fn default_expiry() -> DateTime<Utc> {
32 Utc::now() + Duration::seconds(SESSION_MAX_AGE_SECS as i64)
33}
34
35#[cfg(feature = "server")]
36pub fn session_cookie(token: &str, max_age_seconds: i32, secure: bool) -> String {
38 let secure_flag = if secure { "; Secure" } else { "" };
39 format!(
40 "session={token}; HttpOnly; Path=/; Max-Age={max_age_seconds}; SameSite=Lax{secure_flag}"
41 )
42}
43
44#[cfg(feature = "server")]
45pub fn parse_session_token(cookie_header: &str) -> Option<&str> {
47 cookie_header.split(';').map(|s| s.trim()).find_map(|pair| {
48 let mut parts = pair.splitn(2, '=');
49 let name = parts.next()?.trim();
50 let value = parts.next()?.trim();
51 if name == "session" {
52 Some(value)
53 } else {
54 None
55 }
56 })
57}
58
59#[cfg(feature = "server")]
60pub fn get_session_from_ctx() -> Option<String> {
62 crate::request_ctx::request_headers().and_then(|headers| {
63 headers
64 .get("cookie")
65 .and_then(|h| h.to_str().ok())
66 .and_then(parse_session_token)
67 .map(|s| s.to_string())
68 })
69}
70
71#[cfg(all(test, feature = "server"))]
72mod tests {
73 use super::*;
74 use sha2::Digest; #[test]
77 fn parse_session_found() {
78 let header = "session=abc123; path=/";
79 assert_eq!(parse_session_token(header), Some("abc123"));
80 }
81
82 #[test]
83 fn parse_session_single_cookie() {
84 assert_eq!(parse_session_token("session=token456"), Some("token456"));
85 }
86
87 #[test]
88 fn parse_session_not_found() {
89 assert_eq!(parse_session_token("other=value"), None);
90 }
91
92 #[test]
93 fn parse_session_empty_string() {
94 assert_eq!(parse_session_token(""), None);
95 }
96
97 #[test]
98 fn parse_session_multiple_cookies() {
99 let header = "theme=dark; session=my-secret; lang=en";
100 assert_eq!(parse_session_token(header), Some("my-secret"));
101 }
102
103 #[test]
104 fn parse_session_empty_value() {
105 assert_eq!(parse_session_token("session="), Some(""));
106 }
107
108 #[test]
109 fn parse_session_trailing_semicolon() {
110 assert_eq!(parse_session_token("session=abc;"), Some("abc"));
111 }
112
113 #[test]
114 fn generate_token_is_uuid() {
115 let token = generate_token();
116 assert!(uuid::Uuid::parse_str(&token).is_ok());
117 }
118
119 #[test]
120 fn default_expiry_is_future() {
121 let expiry = default_expiry();
122 assert!(expiry > chrono::Utc::now());
123 }
124
125 #[test]
126 fn default_expiry_is_about_30_days() {
127 let expiry = default_expiry();
128 let diff = expiry - chrono::Utc::now();
129 assert!(diff.num_days() >= 29 && diff.num_days() <= 31);
130 }
131
132 #[test]
133 fn hash_token_is_deterministic() {
134 let token = "test-token-123";
135 assert_eq!(hash_token(token), hash_token(token));
136 }
137
138 #[test]
139 fn hash_token_is_64_chars() {
140 let hash = hash_token("any-token");
141 assert_eq!(hash.len(), 64);
142 }
143
144 #[test]
145 fn hash_token_differs_from_input() {
146 let token = "my-secret-token";
147 assert_ne!(hash_token(token), token);
148 }
149
150 #[test]
151 fn hash_token_known_value() {
152 let hash = hash_token("hello");
153 let expected = sha2::Sha256::digest(b"hello");
154 assert_eq!(hash, hex::encode(expected));
155 }
156
157 #[test]
158 fn session_cookie_without_secure() {
159 let cookie = session_cookie("abc", 3600, false);
160 assert!(cookie.contains("session=abc"));
161 assert!(cookie.contains("HttpOnly"));
162 assert!(cookie.contains("SameSite=Lax"));
163 assert!(!cookie.contains("Secure"));
164 }
165
166 #[test]
167 fn session_cookie_with_secure() {
168 let cookie = session_cookie("abc", 3600, true);
169 assert!(cookie.contains("Secure"));
170 }
171
172 #[test]
173 fn session_cookie_logout_has_zero_max_age() {
174 let cookie = session_cookie("", 0, false);
175 assert!(cookie.contains("Max-Age=0"));
176 }
177}