Skip to main content

actix_cloud/session/
middleware.rs

1use std::{borrow::Cow, fmt, future::Future, pin::Pin, rc::Rc, sync::Arc};
2
3use actix_utils::future::{ready, Ready};
4use actix_web::{
5    body::MessageBody,
6    cookie::{time::Duration, Cookie, CookieJar, Key},
7    dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
8    http::header::{HeaderValue, SET_COOKIE},
9    HttpResponse,
10};
11use serde_json::{Map, Value};
12
13use super::{
14    config::{
15        self, Configuration, CookieConfiguration, CookieContentSecurity, SessionMiddlewareBuilder,
16        TtlExtensionPolicy,
17    },
18    storage::{SessionKey, SessionStore},
19    Session, SessionStatus,
20};
21use crate::{memorydb::MemoryDB, Result};
22
23/// A middleware for session management in Actix Web applications.
24///
25/// [`SessionMiddleware`] takes care of a few jobs:
26///
27/// - Instructs the session storage backend to create/update/delete/retrieve the state attached to
28///   a session according to its status and the operations that have been performed against it;
29/// - Set/remove a cookie, on the client side, to enable a user to be consistently associated with
30///   the same session across multiple HTTP requests.
31///
32/// Use [`SessionMiddleware::new`] to initialize the session framework using the default parameters.
33/// To create a new instance of [`SessionMiddleware`] you need to provide:
34///
35/// - an instance of the session storage backend you wish to use (i.e. an implementation of
36///   [`SessionStore`]);
37/// - a secret key, to sign or encrypt the content of client-side session cookie.
38///
39/// # How did we choose defaults?
40/// You should not regret adding `actix-session` to your dependencies and going to production using
41/// the default configuration. That is why, when in doubt, we opt to use the most secure option for
42/// each configuration parameter.
43///
44/// We expose knobs to change the default to suit your needs—i.e., if you know what you are doing,
45/// we will not stop you. But being a subject-matter expert should not be a requirement to deploy
46/// reasonably secure implementation of sessions.
47#[derive(Clone)]
48pub struct SessionMiddleware {
49    storage_backend: Rc<SessionStore>,
50    configuration: Rc<Configuration>,
51}
52
53impl SessionMiddleware {
54    /// Use [`SessionMiddleware::new`] to initialize the session framework using the default
55    /// parameters.
56    ///
57    /// To create a new instance of [`SessionMiddleware`] you need to provide:
58    /// - an instance of the session storage backend you wish to use (i.e. an implementation of
59    ///   [`SessionStore`]);
60    /// - a secret key, to sign or encrypt the content of client-side session cookie.
61    pub fn new(client: Arc<dyn MemoryDB>, key: Key) -> Self {
62        Self::builder(client, key).build()
63    }
64
65    /// A fluent API to configure [`SessionMiddleware`].
66    ///
67    /// It takes as input the two required inputs to create a new instance of [`SessionMiddleware`]:
68    /// - an instance of the session storage backend you wish to use (i.e. an implementation of
69    ///   [`SessionStore`]);
70    /// - a secret key, to sign or encrypt the content of client-side session cookie.
71    pub fn builder(client: Arc<dyn MemoryDB>, key: Key) -> SessionMiddlewareBuilder {
72        SessionMiddlewareBuilder::new(client, config::default_configuration(key))
73    }
74
75    pub(crate) fn from_parts(store: SessionStore, configuration: Configuration) -> Self {
76        Self {
77            storage_backend: Rc::new(store),
78            configuration: Rc::new(configuration),
79        }
80    }
81}
82
83impl<S, B> Transform<S, ServiceRequest> for SessionMiddleware
84where
85    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
86    S::Future: 'static,
87    B: MessageBody + 'static,
88{
89    type Response = ServiceResponse<B>;
90    type Error = actix_web::Error;
91    type Transform = InnerSessionMiddleware<S>;
92    type InitError = ();
93    type Future = Ready<Result<Self::Transform, Self::InitError>>;
94
95    fn new_transform(&self, service: S) -> Self::Future {
96        ready(Ok(InnerSessionMiddleware {
97            service: Rc::new(service),
98            configuration: Rc::clone(&self.configuration),
99            storage_backend: Rc::clone(&self.storage_backend),
100        }))
101    }
102}
103
104/// Short-hand to create an `actix_web::Error` instance that will result in an `Internal Server
105/// Error` response while preserving the error root cause (e.g. in logs).
106fn e500<E: fmt::Debug + fmt::Display + 'static>(err: E) -> actix_web::Error {
107    // We do not use `actix_web::error::ErrorInternalServerError` because we do not want to
108    // leak internal implementation details to the caller.
109    //
110    // `actix_web::error::ErrorInternalServerError` includes the error Display representation
111    // as body of the error responses, leading to messages like "There was an issue persisting
112    // the session state" reaching API clients. We don't want that, we want opaque 500s.
113    actix_web::error::InternalError::from_response(
114        err,
115        HttpResponse::InternalServerError().finish(),
116    )
117    .into()
118}
119
120#[doc(hidden)]
121#[non_exhaustive]
122pub struct InnerSessionMiddleware<S> {
123    service: Rc<S>,
124    configuration: Rc<Configuration>,
125    storage_backend: Rc<SessionStore>,
126}
127
128impl<S, B> Service<ServiceRequest> for InnerSessionMiddleware<S>
129where
130    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
131    S::Future: 'static,
132{
133    type Response = ServiceResponse<B>;
134    type Error = actix_web::Error;
135    #[allow(clippy::type_complexity)]
136    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
137
138    forward_ready!(service);
139
140    fn call(&self, mut req: ServiceRequest) -> Self::Future {
141        let service = Rc::clone(&self.service);
142        let storage_backend = Rc::clone(&self.storage_backend);
143        let configuration = Rc::clone(&self.configuration);
144
145        Box::pin(async move {
146            let session_key = extract_session_key(&req, &configuration.cookie);
147            let (session_key, session_state) =
148                load_session_state(session_key, storage_backend.as_ref()).await?;
149
150            Session::set_session(&mut req, session_state);
151
152            let mut res = service.call(req).await?;
153            let (status, session_state) = Session::get_changes(&mut res);
154
155            let mut ttl = configuration.session.state_ttl;
156            let mut cookie = Cow::Borrowed(&configuration.cookie);
157            if let Some(x) = session_state.get("_ttl") {
158                if let Some(x) = x.as_i64() {
159                    ttl = Duration::milliseconds(x);
160                    let mut tmp = cookie.into_owned();
161                    tmp.max_age = Some(ttl);
162                    cookie = Cow::Owned(tmp);
163                }
164            }
165            let id = session_state
166                .get("_id")
167                .map(|x| x.to_string().trim_matches('"').to_owned());
168
169            match session_key {
170                None => {
171                    // we do not create an entry in the session store if there is no state attached
172                    // to a fresh session
173                    if !session_state.is_empty() {
174                        let session_key = storage_backend
175                            .save(session_state, &id, &ttl)
176                            .await
177                            .map_err(e500)?;
178
179                        set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
180                            .map_err(e500)?;
181                    }
182                }
183
184                Some(session_key) => {
185                    match status {
186                        SessionStatus::Changed => {
187                            let session_key = storage_backend
188                                .update(session_key, session_state, &id, &ttl)
189                                .await
190                                .map_err(e500)?;
191
192                            set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
193                                .map_err(e500)?;
194                        }
195
196                        SessionStatus::Purged => {
197                            storage_backend
198                                .delete(&session_key, &id)
199                                .await
200                                .map_err(e500)?;
201
202                            delete_session_cookie(res.response_mut().head_mut(), &cookie)
203                                .map_err(e500)?;
204                        }
205
206                        SessionStatus::Renewed => {
207                            storage_backend
208                                .delete(&session_key, &id)
209                                .await
210                                .map_err(e500)?;
211
212                            let session_key = storage_backend
213                                .save(session_state, &id, &ttl)
214                                .await
215                                .map_err(e500)?;
216
217                            set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
218                                .map_err(e500)?;
219                        }
220
221                        SessionStatus::Unchanged => {
222                            if matches!(
223                                configuration.ttl_extension_policy,
224                                TtlExtensionPolicy::OnEveryRequest
225                            ) {
226                                storage_backend
227                                    .update_ttl(&session_key, &id, &ttl)
228                                    .await
229                                    .map_err(e500)?;
230
231                                if configuration.cookie.max_age.is_some() {
232                                    set_session_cookie(
233                                        res.response_mut().head_mut(),
234                                        session_key,
235                                        &cookie,
236                                    )
237                                    .map_err(e500)?;
238                                }
239                            }
240                        }
241                    };
242                }
243            }
244
245            Ok(res)
246        })
247    }
248}
249
250/// Examines the session cookie attached to the incoming request, if there is one, and tries
251/// to extract the session key.
252///
253/// It returns `None` if there is no session cookie or if the session cookie is considered invalid
254/// (e.g., when failing a signature check).
255fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
256    let cookies = req.cookies().ok()?;
257    let session_cookie = cookies
258        .iter()
259        .find(|&cookie| cookie.name() == config.name)?;
260
261    let mut jar = CookieJar::new();
262    jar.add_original(session_cookie.clone());
263
264    let verification_result = match config.content_security {
265        CookieContentSecurity::Signed => jar.signed(&config.key).get(&config.name),
266        CookieContentSecurity::Private => jar.private(&config.key).get(&config.name),
267    };
268
269    verification_result?.value().to_owned().try_into().ok()
270}
271
272async fn load_session_state(
273    session_key: Option<SessionKey>,
274    storage_backend: &SessionStore,
275) -> Result<(Option<SessionKey>, Map<String, Value>), actix_web::Error> {
276    if let Some(session_key) = session_key {
277        match storage_backend.load(&session_key).await {
278            Ok(state) => {
279                if let Some(state) = state {
280                    Ok((Some(session_key), state))
281                } else {
282                    // We discard the existing session key given that the state attached to it can
283                    // no longer be found (e.g. it expired or we suffered some data loss in the
284                    // storage). Regenerating the session key will trigger the `save` workflow
285                    // instead of the `update` workflow if the session state is modified during the
286                    // lifecycle of the current request.
287
288                    Ok((None, Map::new()))
289                }
290            }
291
292            Err(err) => Err(e500(err)),
293        }
294    } else {
295        Ok((None, Map::new()))
296    }
297}
298
299fn set_session_cookie(
300    response: &mut ResponseHead,
301    session_key: SessionKey,
302    config: &CookieConfiguration,
303) -> Result<()> {
304    let value: String = session_key.into();
305    let mut cookie = Cookie::new(config.name.clone(), value);
306
307    cookie.set_secure(config.secure);
308    cookie.set_http_only(config.http_only);
309    cookie.set_same_site(config.same_site);
310    cookie.set_path(config.path.clone());
311
312    if let Some(max_age) = config.max_age {
313        cookie.set_max_age(max_age);
314    }
315
316    if let Some(ref domain) = config.domain {
317        cookie.set_domain(domain.clone());
318    }
319
320    let mut jar = CookieJar::new();
321    match config.content_security {
322        CookieContentSecurity::Signed => jar.signed_mut(&config.key).add(cookie),
323        CookieContentSecurity::Private => jar.private_mut(&config.key).add(cookie),
324    }
325
326    // set cookie
327    let cookie = jar.delta().next().unwrap();
328    let val = HeaderValue::from_str(&cookie.encoded().to_string())?;
329
330    response.headers_mut().append(SET_COOKIE, val);
331
332    Ok(())
333}
334
335fn delete_session_cookie(response: &mut ResponseHead, config: &CookieConfiguration) -> Result<()> {
336    let removal_cookie = Cookie::build(config.name.clone(), "")
337        .path(config.path.clone())
338        .secure(config.secure)
339        .http_only(config.http_only)
340        .same_site(config.same_site);
341
342    let mut removal_cookie = if let Some(ref domain) = config.domain {
343        removal_cookie.domain(domain)
344    } else {
345        removal_cookie
346    }
347    .finish();
348
349    removal_cookie.make_removal();
350
351    let val = HeaderValue::from_str(&removal_cookie.to_string())?;
352    response.headers_mut().append(SET_COOKIE, val);
353
354    Ok(())
355}