Skip to main content

axum_security/cookie/
session.rs

1use std::{convert::Infallible, hash::Hash};
2
3use axum::{
4    extract::{FromRequestParts, OptionalFromRequestParts},
5    http::{Extensions, StatusCode, request::Parts},
6};
7
8use crate::cookie::SessionId;
9
10/// A server-side session loaded from the cookie store.
11///
12/// Use as a handler extractor to require an active session (returns `401`
13/// if missing). Use `Option<CookieSession<S>>` for optional extraction.
14#[derive(Clone, Debug)]
15pub struct CookieSession<S> {
16    /// The unique session identifier (matches the cookie value).
17    pub session_id: SessionId,
18    /// Unix timestamp (seconds) when the session was created.
19    pub created_at: u64,
20    /// The application-defined session state.
21    pub state: S,
22}
23
24impl<S> CookieSession<S> {
25    pub fn new(id: SessionId, created_at: u64, value: S) -> Self {
26        Self {
27            session_id: id,
28            created_at,
29            state: value,
30        }
31    }
32
33    pub fn from_extensions(extensions: &mut Extensions) -> Option<Self>
34    where
35        S: Send + Sync + 'static,
36    {
37        extensions.remove()
38    }
39}
40
41impl<S> Hash for CookieSession<S> {
42    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
43        self.session_id.hash(state)
44    }
45}
46
47impl<S> Eq for CookieSession<S> {}
48
49impl<S> PartialEq for CookieSession<S> {
50    fn eq(&self, other: &Self) -> bool {
51        self.session_id == other.session_id
52    }
53}
54
55impl<S, T> FromRequestParts<S> for CookieSession<T>
56where
57    S: Send + Sync,
58    T: Send + Sync + 'static,
59{
60    type Rejection = StatusCode;
61
62    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, StatusCode> {
63        if let Some(session) = parts.extensions.remove() {
64            Ok(session)
65        } else {
66            Err(StatusCode::UNAUTHORIZED)
67        }
68    }
69}
70
71impl<S, T> OptionalFromRequestParts<S> for CookieSession<T>
72where
73    S: Send + Sync,
74    T: Send + Sync + 'static,
75{
76    type Rejection = Infallible;
77
78    async fn from_request_parts(
79        parts: &mut Parts,
80        _state: &S,
81    ) -> Result<Option<Self>, Self::Rejection> {
82        Ok(parts.extensions.remove())
83    }
84}
85
86#[cfg(test)]
87mod extract_cookie {
88    use axum::{
89        extract::FromRequestParts,
90        http::{Request, StatusCode},
91    };
92
93    use crate::cookie::{CookieSession, SessionId};
94
95    #[tokio::test]
96    async fn extract() {
97        let cookie = CookieSession::new(SessionId::new(), 0, ());
98
99        let (mut parts, _) = Request::builder()
100            .extension(cookie.clone())
101            .body(())
102            .unwrap()
103            .into_parts();
104
105        let extracted_cookie = CookieSession::<()>::from_request_parts(&mut parts, &())
106            .await
107            .unwrap();
108
109        assert!(cookie.session_id == extracted_cookie.session_id);
110        assert!(cookie.created_at == extracted_cookie.created_at);
111    }
112
113    #[tokio::test]
114    async fn extract_rejection() {
115        let (mut parts, _) = Request::builder().body(()).unwrap().into_parts();
116
117        let rejection = CookieSession::<()>::from_request_parts(&mut parts, &())
118            .await
119            .unwrap_err();
120
121        assert!(rejection == StatusCode::UNAUTHORIZED);
122    }
123}