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