1#![deny(missing_docs)]
88#![deny(rustdoc::broken_intra_doc_links)]
89
90pub mod cache;
91pub mod cookie;
92pub mod database;
93pub mod driver;
94pub mod manager;
95pub mod middleware;
96pub mod session;
97pub mod store;
98
99pub use cache::CacheSessionStore;
100pub use cookie::CookieSessionStore;
101pub use database::{DatabaseSessionStore, SessionRow};
102pub use driver::SessionDriver;
103pub use manager::{SessionConfig, SessionManager};
104pub use middleware::{SessionRequestExt, StartSession};
105pub use session::{generate_session_id, Session, SessionData, TOKEN_KEY};
106pub use store::{is_well_formed, MemorySessionStore, SessionStore};
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use rainier_http::{Request, Response};
112 use rainier_middleware::{Middleware, Pipeline};
113 use std::sync::Arc;
114
115 #[tokio::test]
118 async fn logging_in_rotates_the_session_and_keeps_the_user() {
119 let store = Arc::new(MemorySessionStore::default());
120 let start: Arc<dyn Middleware> =
121 Arc::new(StartSession::new(Arc::clone(&store) as Arc<dyn SessionStore>));
122
123 let run = |cookie: Option<String>, action: &'static str| {
124 let start = Arc::clone(&start);
125 async move {
126 let mut builder = Request::builder();
127 if let Some(cookie) = cookie {
128 builder = builder.header("cookie", &cookie);
129 }
130 Pipeline::new()
131 .through_arc(start)
132 .then(move |request: Request| async move {
133 let session = request.session().expect("a session");
134 match action {
135 "visit" => {
136 session.put("cart", vec![1u64, 2]).unwrap();
137 }
138 "login" => {
139 session.regenerate();
140 session.put("user_id", 7u64).unwrap();
141 }
142 _ => {}
143 }
144 Response::text("ok")
145 })
146 .run(builder.build())
147 .await
148 }
149 };
150
151 let cookie_of = |response: &Response| {
152 response
153 .header("set-cookie")
154 .and_then(|h| h.split(';').next())
155 .and_then(|c| c.strip_prefix("rainier_session="))
156 .map(str::to_string)
157 };
158
159 let first = run(None, "visit").await;
161 let anonymous = cookie_of(&first).expect("a cookie");
162
163 let second = run(Some(format!("rainier_session={anonymous}")), "login").await;
165 let authenticated = cookie_of(&second).expect("a new cookie");
166
167 assert_ne!(authenticated, anonymous, "the id must change on login");
168 assert!(
169 store.read(&anonymous).await.unwrap().is_none(),
170 "the pre-login row is what a fixation attack holds"
171 );
172
173 let data = store.read(&authenticated).await.unwrap().expect("the new session");
174 assert_eq!(data.values["user_id"], 7);
175 assert_eq!(data.values["cart"], serde_json::json!([1, 2]), "the cart survives login");
176 }
177}
178
179#[cfg(test)]
180mod driver_tests {
181 use super::*;
182 use rainier_cache::MemoryCache;
183 use rainier_crypt::{Encryption, Key, KeyRing};
184 use rainier_http::{Request, Response};
185 use rainier_middleware::{Middleware, Pipeline};
186 use std::sync::Arc;
187
188 async fn counts_across_requests(store: Arc<dyn SessionStore>) -> (u64, u64) {
194 let start: Arc<dyn Middleware> = Arc::new(StartSession::new(store));
195
196 let visit = |cookie: Option<String>| {
197 let start = Arc::clone(&start);
198 async move {
199 let mut builder = Request::builder();
200 if let Some(cookie) = cookie {
201 builder = builder.header("cookie", &cookie);
202 }
203
204 let response = Pipeline::new()
205 .through_arc(start)
206 .then(|request: Request| async move {
207 let session = request.session().expect("a session");
208 let seen: u64 = session.get("seen").unwrap_or(0);
209 session.put("seen", seen + 1).unwrap();
210 Response::text(seen.to_string())
211 })
212 .run(builder.build())
213 .await;
214
215 let cookie = response
216 .header("set-cookie")
217 .and_then(|h| h.split(';').next())
218 .and_then(|c| c.strip_prefix("rainier_session="))
219 .map(str::to_string);
220
221 let body = response.into_http().into_body().collect().await.unwrap();
222 (String::from_utf8(body.to_vec()).unwrap().parse::<u64>().unwrap(), cookie)
223 }
224 };
225
226 let (first, cookie) = visit(None).await;
227 let cookie = cookie.expect("the session should be persisted");
228 let (second, _) = visit(Some(format!("rainier_session={cookie}"))).await;
229
230 (first, second)
231 }
232
233 fn crypt() -> Encryption {
234 Encryption::from_keys(KeyRing::new(Key::generate()))
235 }
236
237 #[tokio::test]
238 async fn every_driver_persists_a_session_across_requests() {
239 let drivers: Vec<(&str, Arc<dyn SessionStore>)> = vec![
240 ("memory", Arc::new(MemorySessionStore::default())),
241 ("cache", Arc::new(CacheSessionStore::new(Arc::new(MemoryCache::new())))),
242 ("cookie", Arc::new(CookieSessionStore::new(crypt()))),
243 ];
244
245 for (name, store) in drivers {
246 assert_eq!(counts_across_requests(store).await, (0, 1), "{name}");
247 }
248 }
249
250 #[tokio::test]
251 async fn the_cookie_driver_carries_the_session_and_not_an_id() {
252 let store: Arc<dyn SessionStore> = Arc::new(CookieSessionStore::new(crypt()));
255 let start: Arc<dyn Middleware> = Arc::new(StartSession::new(Arc::clone(&store)));
256
257 let response = Pipeline::new()
258 .through_arc(start)
259 .then(|request: Request| async move {
260 request.session().unwrap().put("user_id", 42u64).unwrap();
261 Response::text("ok")
262 })
263 .run(Request::builder().build())
264 .await;
265
266 let value = response
267 .header("set-cookie")
268 .and_then(|h| h.split(';').next())
269 .and_then(|c| c.strip_prefix("rainier_session="))
270 .expect("a cookie");
271
272 assert!(value.len() > 64, "a cookie session is longer than an id: {}", value.len());
273 assert!(!is_well_formed(value), "it is not an id at all");
274
275 assert!(!value.contains("user_id"), "the client cannot read it: {value}");
279 }
280
281 #[tokio::test]
282 async fn a_cookie_session_from_another_applications_key_is_discarded() {
283 let store: Arc<dyn SessionStore> = Arc::new(CookieSessionStore::new(crypt()));
284 let theirs = CookieSessionStore::new(crypt());
285
286 let forged = theirs.encode(&generate_session_id(), &SessionData::default()).unwrap();
287 let start: Arc<dyn Middleware> = Arc::new(StartSession::new(store));
288
289 let response = Pipeline::new()
290 .through_arc(start)
291 .then(|request: Request| async move {
292 let session = request.session().expect("a fresh session");
293 Response::text(session.is_empty().to_string())
294 })
295 .run(Request::builder().header("cookie", &format!("rainier_session={forged}")).build())
296 .await;
297
298 let body = response.into_http().into_body().collect().await.unwrap();
299 assert_eq!(body, "true", "a session we did not write must not be trusted");
300 }
301}