actix_extended_session/
middleware.rs

1use crate::config::SessionLifecycle;
2use crate::{
3    config::{self, Configuration, CookieConfiguration, SessionMiddlewareBuilder},
4    storage::{LoadError, SessionKey, SessionStore},
5    Session, SessionStatus,
6};
7use actix_utils::future::{ready, Ready};
8use actix_web::{
9    body::MessageBody,
10    cookie::{Cookie, CookieJar, Key},
11    dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
12    http::header::{HeaderValue, SET_COOKIE},
13    HttpResponse,
14};
15use anyhow::Context;
16use serde_json::{Map, Value};
17use std::{convert::TryInto, fmt, future::Future, pin::Pin, rc::Rc};
18
19/// A middleware for session management in Actix Web applications.
20///
21/// [`SessionMiddleware`] takes care of a few jobs:
22///
23/// - Instructs the session storage backend to create/update/delete/retrieve the state attached to
24///   a session according to its status and the operations that have been performed against it;
25/// - Set/remove a cookie, on the client side, to enable a user to be consistently associated with
26///   the same session across multiple HTTP requests.
27///
28/// Use [`SessionMiddleware::new`] to initialize the session framework using the default parameters.
29/// To create a new instance of [`SessionMiddleware`] you need to provide:
30///
31/// - an instance of the session storage backend you wish to use (i.e. an implementation of
32///   [`SessionStore`]);
33/// - a secret key, to sign or encrypt the content of client-side session cookie.
34///
35/// # Examples
36/// ```no_run
37/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
38/// use actix_extended_session::{Session, SessionMiddleware, storage::CookieSessionStore};
39/// use actix_web::cookie::Key;
40///
41/// // The secret key would usually be read from a configuration file/environment variables.
42/// fn get_secret_key() -> Key {
43///     # todo!()
44///     // [...]
45/// }
46///
47/// #[actix_web::main]
48/// async fn main() -> std::io::Result<()> {
49///     let secret_key = get_secret_key();
50///     HttpServer::new(move ||
51///             App::new()
52///             // Add session management to your application using Redis for session state storage
53///             .wrap(
54///                 SessionMiddleware::new(
55///                     CookieSessionStore::default(),
56///                     secret_key.clone()
57///                 )
58///             )
59///             .default_service(web::to(|| HttpResponse::Ok())))
60///         .bind(("127.0.0.1", 8080))?
61///         .run()
62///         .await
63/// }
64/// ```
65///
66/// If you want to customise use [`builder`](Self::builder) instead of [`new`](Self::new):
67///
68/// ```no_run
69/// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web};
70/// use actix_extended_session::{Session, SessionMiddleware, storage::CookieSessionStore};
71///
72/// // The secret key would usually be read from a configuration file/environment variables.
73/// fn get_secret_key() -> Key {
74///     # todo!()
75///     // [...]
76/// }
77///
78/// #[actix_web::main]
79/// async fn main() -> std::io::Result<()> {
80///     let secret_key = get_secret_key();
81///     HttpServer::new(move ||
82///             App::new()
83///             // Customise session length!
84///             .wrap(
85///                 SessionMiddleware::builder(
86///                     CookieSessionStore::default(),
87///                     secret_key.clone()
88///                 )
89///                 .session_ttl(time::Duration::days(5))
90///                 .build(),
91///             )
92///             .default_service(web::to(|| HttpResponse::Ok())))
93///         .bind(("127.0.0.1", 8080))?
94///         .run()
95///         .await
96/// }
97/// ```
98#[derive(Clone)]
99pub struct SessionMiddleware<Store: SessionStore> {
100    storage_backend: Rc<Store>,
101    configuration: Rc<Configuration>,
102}
103
104impl<Store: SessionStore> SessionMiddleware<Store> {
105    /// Use [`SessionMiddleware::new`] to initialize the session framework using the default
106    /// parameters.
107    ///
108    /// To create a new instance of [`SessionMiddleware`] you need to provide:
109    /// - an instance of the session storage backend you wish to use (i.e. an implementation of
110    ///   [`SessionStore`]);
111    /// - a secret key, to sign or encrypt the content of client-side session cookie.
112    pub fn new(store: Store, key: Key) -> Self {
113        Self::builder(store, key).build()
114    }
115
116    /// A fluent API to configure [`SessionMiddleware`].
117    ///
118    /// It takes as input the two required inputs to create a new instance of [`SessionMiddleware`]:
119    /// - an instance of the session storage backend you wish to use (i.e. an implementation of
120    ///   [`SessionStore`]);
121    /// - a secret key, to sign or encrypt the content of client-side session cookie.
122    pub fn builder(store: Store, key: Key) -> SessionMiddlewareBuilder<Store> {
123        SessionMiddlewareBuilder::new(store, config::default_configuration(key))
124    }
125
126    pub(crate) fn from_parts(store: Store, configuration: Configuration) -> Self {
127        Self {
128            storage_backend: Rc::new(store),
129            configuration: Rc::new(configuration),
130        }
131    }
132}
133
134impl<S, B, Store> Transform<S, ServiceRequest> for SessionMiddleware<Store>
135where
136    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
137    S::Future: 'static,
138    B: MessageBody + 'static,
139    Store: SessionStore + 'static,
140{
141    type Response = ServiceResponse<B>;
142    type Error = actix_web::Error;
143    type Transform = InnerSessionMiddleware<S, Store>;
144    type InitError = ();
145    type Future = Ready<Result<Self::Transform, Self::InitError>>;
146
147    fn new_transform(&self, service: S) -> Self::Future {
148        ready(Ok(InnerSessionMiddleware {
149            service: Rc::new(service),
150            configuration: Rc::clone(&self.configuration),
151            storage_backend: Rc::clone(&self.storage_backend),
152        }))
153    }
154}
155
156/// Short-hand to create an `actix_web::Error` instance that will result in an `Internal Server
157/// Error` response while preserving the error root cause (e.g. in logs).
158fn e500<E: fmt::Debug + fmt::Display + 'static>(err: E) -> actix_web::Error {
159    // We do not use `actix_web::error::ErrorInternalServerError` because we do not want to
160    // leak internal implementation details to the caller.
161    //
162    // `actix_web::error::ErrorInternalServerError` includes the error Display representation
163    // as body of the error responses, leading to messages like "There was an issue persisting
164    // the session state" reaching API clients. We don't want that, we want opaque 500s.
165    actix_web::error::InternalError::from_response(
166        err,
167        HttpResponse::InternalServerError().finish(),
168    )
169    .into()
170}
171
172static LIFECYCLE_KEY: &str = "lifecycle";
173
174#[doc(hidden)]
175#[non_exhaustive]
176pub struct InnerSessionMiddleware<S, Store: SessionStore + 'static> {
177    service: Rc<S>,
178    configuration: Rc<Configuration>,
179    storage_backend: Rc<Store>,
180}
181
182impl<S, B, Store> Service<ServiceRequest> for InnerSessionMiddleware<S, Store>
183where
184    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
185    S::Future: 'static,
186    Store: SessionStore + 'static,
187{
188    type Response = ServiceResponse<B>;
189    type Error = actix_web::Error;
190    #[allow(clippy::type_complexity)]
191    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
192
193    forward_ready!(service);
194
195    fn call(&self, mut req: ServiceRequest) -> Self::Future {
196        let service = Rc::clone(&self.service);
197        let storage_backend = Rc::clone(&self.storage_backend);
198        let configuration = Rc::clone(&self.configuration);
199
200        Box::pin(async move {
201            let session_key = extract_session_key(&req, &configuration.cookie);
202            let (session_key, session_state) =
203                load_session_state(session_key, storage_backend.as_ref()).await?;
204            let mut session_lifecycle = SessionLifecycle::PersistentSession;
205
206            if let Some(lifecycle) = session_state.get(LIFECYCLE_KEY) {
207                let lifecycle = lifecycle
208                    .as_i64()
209                    .unwrap_or(SessionLifecycle::PersistentSession as i64);
210
211                session_lifecycle = SessionLifecycle::from_i32(lifecycle as i32);
212            }
213
214            Session::set_session(&mut req, session_state, session_lifecycle);
215
216            let mut res = service.call(req).await?;
217            let (lifecycle, status, mut session_state) = Session::get_changes(&mut res);
218
219            // We only insert the lifecycle key into the session if it already exist or is non-empty to avoid creating sessions on every request.
220            if session_key.is_some() || !session_state.is_empty() {
221                session_state.insert(
222                    LIFECYCLE_KEY.to_string(),
223                    Value::from(lifecycle.clone() as i32),
224                );
225            }
226
227            match session_key {
228                None => {
229                    // We do not create an entry in the session store if there is no state attached
230                    // to a fresh session
231                    if !session_state.is_empty() {
232                        let session_key = storage_backend
233                            .save(session_state, &configuration.session.state_ttl)
234                            .await
235                            .map_err(e500)?;
236
237                        set_session_cookie(
238                            res.response_mut().head_mut(),
239                            session_key,
240                            &configuration.cookie,
241                            lifecycle,
242                        )
243                        .map_err(e500)?;
244                    }
245                }
246
247                Some(session_key) => {
248                    match status {
249                        SessionStatus::Changed => {
250                            let session_key = storage_backend
251                                .update(
252                                    session_key,
253                                    session_state,
254                                    &configuration.session.state_ttl,
255                                )
256                                .await
257                                .map_err(e500)?;
258
259                            set_session_cookie(
260                                res.response_mut().head_mut(),
261                                session_key,
262                                &configuration.cookie,
263                                lifecycle,
264                            )
265                            .map_err(e500)?;
266                        }
267
268                        SessionStatus::Purged => {
269                            storage_backend.delete(&session_key).await.map_err(e500)?;
270
271                            delete_session_cookie(
272                                res.response_mut().head_mut(),
273                                &configuration.cookie,
274                            )
275                            .map_err(e500)?;
276                        }
277
278                        SessionStatus::Renewed => {
279                            storage_backend.delete(&session_key).await.map_err(e500)?;
280
281                            let session_key = storage_backend
282                                .save(session_state, &configuration.session.state_ttl)
283                                .await
284                                .map_err(e500)?;
285
286                            set_session_cookie(
287                                res.response_mut().head_mut(),
288                                session_key,
289                                &configuration.cookie,
290                                lifecycle,
291                            )
292                            .map_err(e500)?;
293                        }
294
295                        SessionStatus::Unchanged => {}
296                    };
297                }
298            }
299
300            Ok(res)
301        })
302    }
303}
304
305/// Examines the session cookie attached to the incoming request, if there is one, and tries
306/// to extract the session key.
307///
308/// It returns `None` if there is no session cookie or if the session cookie is considered invalid
309/// (e.g., when failing a signature check).
310fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
311    let cookies = req.cookies().ok()?;
312    let session_cookie = cookies
313        .iter()
314        .find(|&cookie| cookie.name() == config.name)?;
315
316    let mut jar = CookieJar::new();
317    jar.add_original(session_cookie.clone());
318
319    let verification_result = jar.signed(&config.key).get(&config.name);
320
321    if verification_result.is_none() {
322        tracing::warn!(
323            "The session cookie attached to the incoming request failed to pass cryptographic \
324            checks (signature verification/decryption)."
325        );
326    }
327
328    match verification_result?.value().to_owned().try_into() {
329        Ok(session_key) => Some(session_key),
330        Err(err) => {
331            tracing::warn!(
332                error.message = %err,
333                error.cause_chain = ?err,
334                "Invalid session key, ignoring."
335            );
336
337            None
338        }
339    }
340}
341
342async fn load_session_state<Store: SessionStore>(
343    session_key: Option<SessionKey>,
344    storage_backend: &Store,
345) -> Result<(Option<SessionKey>, Map<String, Value>), actix_web::Error> {
346    if let Some(session_key) = session_key {
347        match storage_backend.load(&session_key).await {
348            Ok(state) => {
349                if let Some(state) = state {
350                    Ok((Some(session_key), state))
351                } else {
352                    // We discard the existing session key given that the state attached to it can
353                    // no longer be found (e.g. it expired or we suffered some data loss in the
354                    // storage). Regenerating the session key will trigger the `save` workflow
355                    // instead of the `update` workflow if the session state is modified during the
356                    // lifecycle of the current request.
357
358                    tracing::info!(
359                        "No session state has been found for a valid session key, creating a new \
360                        empty session."
361                    );
362
363                    Ok((None, Map::new()))
364                }
365            }
366
367            Err(err) => match err {
368                LoadError::Deserialization(err) => {
369                    tracing::warn!(
370                        error.message = %err,
371                        error.cause_chain = ?err,
372                        "Invalid session state, creating a new empty session."
373                    );
374
375                    Ok((Some(session_key), Map::new()))
376                }
377
378                LoadError::Other(err) => Err(e500(err)),
379            },
380        }
381    } else {
382        Ok((None, Map::new()))
383    }
384}
385
386fn set_session_cookie(
387    response: &mut ResponseHead,
388    session_key: SessionKey,
389    config: &CookieConfiguration,
390    session_lifecycle: SessionLifecycle,
391) -> Result<(), anyhow::Error> {
392    let value: String = session_key.into();
393    let mut cookie = Cookie::new(config.name.clone(), value);
394
395    cookie.set_secure(config.secure);
396    cookie.set_http_only(config.http_only);
397    cookie.set_same_site(config.same_site);
398    cookie.set_path(config.path.clone());
399
400    // Check for a persistent session.
401    if session_lifecycle == SessionLifecycle::PersistentSession {
402        if let Some(max_age) = config.max_age {
403            cookie.set_max_age(max_age);
404        }
405    }
406
407    if let Some(ref domain) = config.domain {
408        cookie.set_domain(domain.clone());
409    }
410
411    let mut jar = CookieJar::new();
412    jar.signed_mut(&config.key).add(cookie);
413
414    // Set cookie
415    let cookie = jar.delta().next().unwrap();
416    let val = HeaderValue::from_str(&cookie.encoded().to_string())
417        .context("Failed to attach a session cookie to the outgoing response")?;
418
419    response.headers_mut().append(SET_COOKIE, val);
420
421    Ok(())
422}
423
424fn delete_session_cookie(
425    response: &mut ResponseHead,
426    config: &CookieConfiguration,
427) -> Result<(), anyhow::Error> {
428    let removal_cookie = Cookie::build(config.name.clone(), "")
429        .path(config.path.clone())
430        .secure(config.secure)
431        .http_only(config.http_only)
432        .same_site(config.same_site);
433
434    let mut removal_cookie = if let Some(ref domain) = config.domain {
435        removal_cookie.domain(domain)
436    } else {
437        removal_cookie
438    }
439    .finish();
440
441    removal_cookie.make_removal();
442
443    let val = HeaderValue::from_str(&removal_cookie.to_string())
444        .context("Failed to attach a session removal cookie to the outgoing response")?;
445    response.headers_mut().append(SET_COOKIE, val);
446
447    Ok(())
448}