Line data Source code
1 : //! # Session
2 : //!
3 : //! A session associates an cookie with a user.
4 : //!
5 : //! ## TODO
6 : //! - [ ] Sign the session cookie to detect client-side tampering of the cookie.
7 : //! - [ ] Session time-based expiration.
8 : //!
9 : //! [owasp]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
10 :
11 : use crate::responders::ResponseError;
12 : use crate::user::User;
13 : use axum::extract::{FromRef, FromRequestParts, OptionalFromRequestParts};
14 : use axum_extra::extract::cookie::{Key, SignedCookieJar};
15 : use bliki_session::generate_session;
16 : use http::request::Parts;
17 : use rusqlite::Connection;
18 : use std::sync::{Arc, Mutex};
19 :
20 : /// Maps a session_id to a user.
21 : pub(crate) struct Session {
22 : /// A randomly generated unique identifier. Should have at least 64 bits of entropy.
23 : pub session_id: String,
24 : /// The user id
25 : user_id: i64,
26 : }
27 :
28 5 : pub(crate) fn session_new(db: &Connection, user_id: i64) -> anyhow::Result<Session> {
29 5 : let session_id = generate_session();
30 5 : let session = Session { session_id, user_id };
31 5 : let mut stmt = db.prepare("INSERT INTO sessions (session_id, user_id) VALUES (?, ?)")?;
32 5 : let _ret = stmt.insert((&session.session_id, &session.user_id))?;
33 5 : Ok(session)
34 5 : }
35 :
36 : impl<S> FromRequestParts<S> for User
37 : where
38 : S: Send + Sync,
39 : Key: FromRef<S>,
40 : Arc<Mutex<Connection>>: FromRef<S>,
41 : {
42 : type Rejection = ResponseError;
43 :
44 3 : async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
45 3 : let user = fetch_user(parts, state)?;
46 3 : if let Some(user) = user { Ok(user) } else { Err(ResponseError::unauthorized()) }
47 3 : }
48 : }
49 :
50 : impl<S> OptionalFromRequestParts<S> for User
51 : where
52 : S: Send + Sync,
53 : // XXX: Why do I need add these bounds?
54 : Key: FromRef<S>,
55 : Arc<Mutex<Connection>>: FromRef<S>,
56 : {
57 : type Rejection = ResponseError;
58 :
59 : /// If the session cookie, sid, is set, return the User if exists.
60 3 : async fn from_request_parts(
61 3 : parts: &mut Parts,
62 3 : state: &S,
63 3 : ) -> Result<Option<Self>, Self::Rejection> {
64 3 : fetch_user(parts, state)
65 3 : }
66 : }
67 :
68 6 : fn fetch_user<S>(parts: &mut Parts, state: &S) -> Result<Option<User>, ResponseError>
69 6 : where
70 6 : Key: FromRef<S>,
71 6 : Arc<Mutex<Connection>>: FromRef<S>,
72 : {
73 6 : let key = Key::from_ref(state);
74 6 : let jar = SignedCookieJar::from_headers(&parts.headers, key);
75 6 : let Some(cookie) = jar.get("sid") else {
76 3 : return Ok(None);
77 : };
78 3 : let sid = cookie.value_trimmed();
79 3 : let dbmux = Arc::<Mutex<Connection>>::from_ref(state);
80 3 : let db = dbmux.lock()?;
81 3 : let Ok(user_id) = user_from_session(&db, sid) else {
82 0 : return Ok(None);
83 : };
84 3 : let Ok(user) = crate::user::find_by_id(&db, user_id) else {
85 0 : return Ok(None);
86 : };
87 3 : Ok(user)
88 6 : }
89 :
90 : // TODO: Use new types
91 : /// Returns the user_id from the session_id
92 3 : fn user_from_session(db: &Connection, session_id: &str) -> rusqlite::Result<i64> {
93 3 : let mut stmt = db.prepare("SELECT user_id FROM sessions WHERE session_id = ? ")?;
94 3 : let user_id = stmt.query_row([session_id], |row| row.get("user_id"))?;
95 3 : Ok(user_id)
96 3 : }
97 :
98 : #[cfg(test)]
99 : mod tests {
100 : use crate::db::setup_db;
101 :
102 : use super::*;
103 : use googletest::prelude::*;
104 :
105 : // TODO: Remove test once we have an 'integration test'
106 : #[gtest]
107 : fn test_session_new() -> Result<()> {
108 : let db = setup_db(Some("testdata/test_session_new.sql".into()));
109 : let session = session_new(&db, 1).or_fail()?;
110 : assert_eq!(session.user_id, 1);
111 : Ok(())
112 : }
113 : }
|