actix_session/
lib.rs

1//! Session management for Actix Web.
2//!
3//! The HTTP protocol, at a first glance, is stateless: the client sends a request, the server
4//! parses its content, performs some processing and returns a response. The outcome is only
5//! influenced by the provided inputs (i.e. the request content) and whatever state the server
6//! queries while performing its processing.
7//!
8//! Stateless systems are easier to reason about, but they are not quite as powerful as we need them
9//! to be - e.g. how do you authenticate a user? The user would be forced to authenticate **for
10//! every single request**. That is, for example, how 'Basic' Authentication works. While it may
11//! work for a machine user (i.e. an API client), it is impractical for a person—you do not want a
12//! login prompt on every single page you navigate to!
13//!
14//! There is a solution - **sessions**. Using sessions the server can attach state to a set of
15//! requests coming from the same client. They are built on top of cookies - the server sets a
16//! cookie in the HTTP response (`Set-Cookie` header), the client (e.g. the browser) will store the
17//! cookie and play it back to the server when sending new requests (using the `Cookie` header).
18//!
19//! We refer to the cookie used for sessions as a **session cookie**. Its content is called
20//! **session key** (or **session ID**), while the state attached to the session is referred to as
21//! **session state**.
22//!
23//! `actix-session` provides an easy-to-use framework to manage sessions in applications built on
24//! top of Actix Web. [`SessionMiddleware`] is the middleware underpinning the functionality
25//! provided by `actix-session`; it takes care of all the session cookie handling and instructs the
26//! **storage backend** to create/delete/update the session state based on the operations performed
27//! against the active [`Session`].
28//!
29//! `actix-session` provides some built-in storage backends: ([`CookieSessionStore`],
30//! [`RedisSessionStore`]) - you can create a custom storage backend by implementing the
31//! [`SessionStore`] trait.
32//!
33//! Further reading on sessions:
34//! - [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265);
35//! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
36//!
37//! # Getting started
38//! To start using sessions in your Actix Web application you must register [`SessionMiddleware`]
39//! as a middleware on your `App`:
40//!
41//! ```no_run
42//! use actix_web::{web, App, HttpServer, HttpResponse, Error};
43//! use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
44//! use actix_web::cookie::Key;
45//!
46//! #[actix_web::main]
47//! async fn main() -> std::io::Result<()> {
48//!     // When using `Key::generate()` it is important to initialize outside of the
49//!     // `HttpServer::new` closure. When deployed the secret key should be read from a
50//!     // configuration file or environment variables.
51//!     let secret_key = Key::generate();
52//!
53//!     let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
54//!         .await
55//!         .unwrap();
56//!
57//!     HttpServer::new(move ||
58//!             App::new()
59//!             // Add session management to your application using Redis for session state storage
60//!             .wrap(
61//!                 SessionMiddleware::new(
62//!                     redis_store.clone(),
63//!                     secret_key.clone(),
64//!                 )
65//!             )
66//!             .default_service(web::to(|| HttpResponse::Ok())))
67//!         .bind(("127.0.0.1", 8080))?
68//!         .run()
69//!         .await
70//! }
71//! ```
72//!
73//! The session state can be accessed and modified by your request handlers using the [`Session`]
74//! extractor. Note that this doesn't work in the stream of a streaming response.
75//!
76//! ```no_run
77//! use actix_web::Error;
78//! use actix_session::Session;
79//!
80//! fn index(session: Session) -> Result<&'static str, Error> {
81//!     // access the session state
82//!     if let Some(count) = session.get::<i32>("counter")? {
83//!         println!("SESSION value: {}", count);
84//!         // modify the session state
85//!         session.insert("counter", count + 1)?;
86//!     } else {
87//!         session.insert("counter", 1)?;
88//!     }
89//!
90//!     Ok("Welcome!")
91//! }
92//! ```
93//!
94//! # Choosing A Backend
95//!
96//! By default, `actix-session` does not provide any storage backend to retrieve and save the state
97//! attached to your sessions. You can enable:
98//!
99//! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
100//!   flag.
101//!
102//!   ```console
103//!   cargo add actix-session --features=cookie-session
104//!   ```
105//!
106//! - a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the
107//!   `redis-session` feature flag.
108//!
109//!   ```console
110//!   cargo add actix-session --features=redis-session
111//!   ```
112//!
113//!   Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure
114//!   connection (via the `native-tls` crate):
115//!
116//!   ```console
117//!   cargo add actix-session --features=redis-session-native-tls
118//!   ```
119//!
120//!   If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
121//!
122//!   ```console
123//!   cargo add actix-session --features=redis-session-rustls
124//!   ```
125//!
126//! You can implement your own session storage backend using the [`SessionStore`] trait.
127//!
128//! [`SessionStore`]: storage::SessionStore
129//! [`CookieSessionStore`]: storage::CookieSessionStore
130//! [`RedisSessionStore`]: storage::RedisSessionStore
131
132#![forbid(unsafe_code)]
133#![warn(missing_docs)]
134#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
135#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
136#![cfg_attr(docsrs, feature(doc_auto_cfg))]
137
138pub mod config;
139mod middleware;
140mod session;
141mod session_ext;
142pub mod storage;
143
144pub use self::{
145    middleware::SessionMiddleware,
146    session::{Session, SessionGetError, SessionInsertError, SessionStatus},
147    session_ext::SessionExt,
148};
149
150#[cfg(test)]
151#[allow(missing_docs)]
152pub mod test_helpers {
153    use actix_web::cookie::Key;
154
155    use crate::{config::CookieContentSecurity, storage::SessionStore};
156
157    /// Generate a random cookie signing/encryption key.
158    pub fn key() -> Key {
159        Key::generate()
160    }
161
162    /// A ready-to-go acceptance test suite to verify that sessions behave as expected
163    /// regardless of the underlying session store.
164    ///
165    /// `is_invalidation_supported` must be set to `true` if the backend supports
166    /// "remembering" that a session has been invalidated (e.g. by logging out).
167    /// It should be to `false` if the backend allows multiple cookies to be active
168    /// at the same time (e.g. cookie store backend).
169    pub async fn acceptance_test_suite<F, Store>(store_builder: F, is_invalidation_supported: bool)
170    where
171        Store: SessionStore + 'static,
172        F: Fn() -> Store + Clone + Send + 'static,
173    {
174        for policy in &[
175            CookieContentSecurity::Signed,
176            CookieContentSecurity::Private,
177        ] {
178            println!("Using {policy:?} as cookie content security policy.");
179            acceptance_tests::basic_workflow(store_builder.clone(), *policy).await;
180            acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone(), *policy)
181                .await;
182            acceptance_tests::expiration_is_always_refreshed_if_configured_to_refresh_on_every_request(
183                store_builder.clone(),
184                *policy,
185            )
186            .await;
187            acceptance_tests::complex_workflow(
188                store_builder.clone(),
189                is_invalidation_supported,
190                *policy,
191            )
192            .await;
193            acceptance_tests::guard(store_builder.clone(), *policy).await;
194        }
195    }
196
197    mod acceptance_tests {
198        use actix_web::{
199            cookie::time,
200            dev::{Service, ServiceResponse},
201            guard, middleware, test,
202            web::{self, get, post, resource, Bytes},
203            App, HttpResponse, Result,
204        };
205        use serde::{Deserialize, Serialize};
206        use serde_json::json;
207
208        use crate::{
209            config::{CookieContentSecurity, PersistentSession, TtlExtensionPolicy},
210            storage::SessionStore,
211            test_helpers::key,
212            Session, SessionExt, SessionMiddleware,
213        };
214
215        pub(super) async fn basic_workflow<F, Store>(
216            store_builder: F,
217            policy: CookieContentSecurity,
218        ) where
219            Store: SessionStore + 'static,
220            F: Fn() -> Store + Clone + Send + 'static,
221        {
222            let app = test::init_service(
223                App::new()
224                    .wrap(
225                        SessionMiddleware::builder(store_builder(), key())
226                            .cookie_path("/test/".into())
227                            .cookie_name("actix-test".into())
228                            .cookie_domain(Some("localhost".into()))
229                            .cookie_content_security(policy)
230                            .session_lifecycle(
231                                PersistentSession::default()
232                                    .session_ttl(time::Duration::seconds(100)),
233                            )
234                            .build(),
235                    )
236                    .service(web::resource("/").to(|ses: Session| async move {
237                        let _ = ses.insert("counter", 100);
238                        "test"
239                    }))
240                    .service(web::resource("/test/").to(|ses: Session| async move {
241                        let val: usize = ses.get("counter").unwrap().unwrap();
242                        format!("counter: {val}")
243                    })),
244            )
245            .await;
246
247            let request = test::TestRequest::get().to_request();
248            let response = app.call(request).await.unwrap();
249            let cookie = response.get_cookie("actix-test").unwrap().clone();
250            assert_eq!(cookie.path().unwrap(), "/test/");
251
252            let request = test::TestRequest::with_uri("/test/")
253                .cookie(cookie)
254                .to_request();
255            let body = test::call_and_read_body(&app, request).await;
256            assert_eq!(body, Bytes::from_static(b"counter: 100"));
257        }
258
259        pub(super) async fn expiration_is_always_refreshed_if_configured_to_refresh_on_every_request<
260            F,
261            Store,
262        >(
263            store_builder: F,
264            policy: CookieContentSecurity,
265        ) where
266            Store: SessionStore + 'static,
267            F: Fn() -> Store + Clone + Send + 'static,
268        {
269            let session_ttl = time::Duration::seconds(60);
270            let app = test::init_service(
271                App::new()
272                    .wrap(
273                        SessionMiddleware::builder(store_builder(), key())
274                            .cookie_content_security(policy)
275                            .session_lifecycle(
276                                PersistentSession::default()
277                                    .session_ttl(session_ttl)
278                                    .session_ttl_extension_policy(
279                                        TtlExtensionPolicy::OnEveryRequest,
280                                    ),
281                            )
282                            .build(),
283                    )
284                    .service(web::resource("/").to(|ses: Session| async move {
285                        let _ = ses.insert("counter", 100);
286                        "test"
287                    }))
288                    .service(web::resource("/test/").to(|| async move { "no-changes-in-session" })),
289            )
290            .await;
291
292            // Create session
293            let request = test::TestRequest::get().to_request();
294            let response = app.call(request).await.unwrap();
295            let cookie_1 = response.get_cookie("id").expect("Cookie is set");
296            assert_eq!(cookie_1.max_age(), Some(session_ttl));
297
298            // Fire a request that doesn't touch the session state, check
299            // that the session cookie is present and its expiry is set to the maximum we configured.
300            let request = test::TestRequest::with_uri("/test/")
301                .cookie(cookie_1)
302                .to_request();
303            let response = app.call(request).await.unwrap();
304            let cookie_2 = response.get_cookie("id").expect("Cookie is set");
305            assert_eq!(cookie_2.max_age(), Some(session_ttl));
306        }
307
308        pub(super) async fn expiration_is_refreshed_on_changes<F, Store>(
309            store_builder: F,
310            policy: CookieContentSecurity,
311        ) where
312            Store: SessionStore + 'static,
313            F: Fn() -> Store + Clone + Send + 'static,
314        {
315            let session_ttl = time::Duration::seconds(60);
316            let app = test::init_service(
317                App::new()
318                    .wrap(
319                        SessionMiddleware::builder(store_builder(), key())
320                            .cookie_content_security(policy)
321                            .session_lifecycle(
322                                PersistentSession::default().session_ttl(session_ttl),
323                            )
324                            .build(),
325                    )
326                    .service(web::resource("/").to(|ses: Session| async move {
327                        let _ = ses.insert("counter", 100);
328                        "test"
329                    }))
330                    .service(web::resource("/test/").to(|| async move { "no-changes-in-session" })),
331            )
332            .await;
333
334            let request = test::TestRequest::get().to_request();
335            let response = app.call(request).await.unwrap();
336            let cookie_1 = response.get_cookie("id").expect("Cookie is set");
337            assert_eq!(cookie_1.max_age(), Some(session_ttl));
338
339            let request = test::TestRequest::with_uri("/test/")
340                .cookie(cookie_1.clone())
341                .to_request();
342            let response = app.call(request).await.unwrap();
343            assert!(response.response().cookies().next().is_none());
344
345            let request = test::TestRequest::get().cookie(cookie_1).to_request();
346            let response = app.call(request).await.unwrap();
347            let cookie_2 = response.get_cookie("id").expect("Cookie is set");
348            assert_eq!(cookie_2.max_age(), Some(session_ttl));
349        }
350
351        pub(super) async fn guard<F, Store>(store_builder: F, policy: CookieContentSecurity)
352        where
353            Store: SessionStore + 'static,
354            F: Fn() -> Store + Clone + Send + 'static,
355        {
356            let srv = actix_test::start(move || {
357                App::new()
358                    .wrap(
359                        SessionMiddleware::builder(store_builder(), key())
360                            .cookie_name("test-session".into())
361                            .cookie_content_security(policy)
362                            .session_lifecycle(
363                                PersistentSession::default().session_ttl(time::Duration::days(7)),
364                            )
365                            .build(),
366                    )
367                    .wrap(middleware::Logger::default())
368                    .service(resource("/").route(get().to(index)))
369                    .service(resource("/do_something").route(post().to(do_something)))
370                    .service(resource("/login").route(post().to(login)))
371                    .service(resource("/logout").route(post().to(logout)))
372                    .service(
373                        web::scope("/protected")
374                            .guard(guard::fn_guard(|g| {
375                                g.get_session().get::<String>("user_id").unwrap().is_some()
376                            }))
377                            .service(resource("/count").route(get().to(count))),
378                    )
379            });
380
381            // Step 1: GET without session info
382            //   - response should be a unsuccessful status
383            let req_1 = srv.get("/protected/count").send();
384            let resp_1 = req_1.await.unwrap();
385            assert!(!resp_1.status().is_success());
386
387            // Step 2: POST to login
388            //   - set-cookie actix-session will be in response  (session cookie #1)
389            //   - updates session state: {"counter": 0, "user_id": "ferris"}
390            let req_2 = srv.post("/login").send_json(&json!({"user_id": "ferris"}));
391            let resp_2 = req_2.await.unwrap();
392            let cookie_1 = resp_2
393                .cookies()
394                .unwrap()
395                .clone()
396                .into_iter()
397                .find(|c| c.name() == "test-session")
398                .unwrap();
399
400            // Step 3: POST to do_something
401            //   - adds new session state:  {"counter": 1, "user_id": "ferris" }
402            //   - set-cookie actix-session should be in response (session cookie #2)
403            //   - response should be: {"counter": 1, "user_id": None}
404            let req_3 = srv.post("/do_something").cookie(cookie_1.clone()).send();
405            let mut resp_3 = req_3.await.unwrap();
406            let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
407            assert_eq!(
408                result_3,
409                IndexResponse {
410                    user_id: Some("ferris".into()),
411                    counter: 1
412                }
413            );
414            let cookie_2 = resp_3
415                .cookies()
416                .unwrap()
417                .clone()
418                .into_iter()
419                .find(|c| c.name() == "test-session")
420                .unwrap();
421
422            // Step 4: GET using a existing user id
423            //   - response should be: {"counter": 3, "user_id": "ferris"}
424            let req_4 = srv.get("/protected/count").cookie(cookie_2.clone()).send();
425            let mut resp_4 = req_4.await.unwrap();
426            let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
427            assert_eq!(
428                result_4,
429                IndexResponse {
430                    user_id: Some("ferris".into()),
431                    counter: 1
432                }
433            );
434        }
435
436        pub(super) async fn complex_workflow<F, Store>(
437            store_builder: F,
438            is_invalidation_supported: bool,
439            policy: CookieContentSecurity,
440        ) where
441            Store: SessionStore + 'static,
442            F: Fn() -> Store + Clone + Send + 'static,
443        {
444            let session_ttl = time::Duration::days(7);
445            let srv = actix_test::start(move || {
446                App::new()
447                    .wrap(
448                        SessionMiddleware::builder(store_builder(), key())
449                            .cookie_name("test-session".into())
450                            .cookie_content_security(policy)
451                            .session_lifecycle(
452                                PersistentSession::default().session_ttl(session_ttl),
453                            )
454                            .build(),
455                    )
456                    .wrap(middleware::Logger::default())
457                    .service(resource("/").route(get().to(index)))
458                    .service(resource("/do_something").route(post().to(do_something)))
459                    .service(resource("/login").route(post().to(login)))
460                    .service(resource("/logout").route(post().to(logout)))
461            });
462
463            // Step 1:  GET index
464            //   - set-cookie actix-session should NOT be in response (session data is empty)
465            //   - response should be: {"counter": 0, "user_id": None}
466            let req_1a = srv.get("/").send();
467            let mut resp_1 = req_1a.await.unwrap();
468            assert!(resp_1.cookies().unwrap().is_empty());
469            let result_1 = resp_1.json::<IndexResponse>().await.unwrap();
470            assert_eq!(
471                result_1,
472                IndexResponse {
473                    user_id: None,
474                    counter: 0
475                }
476            );
477
478            // Step 2: POST to do_something
479            //   - adds new session state in redis:  {"counter": 1}
480            //   - set-cookie actix-session should be in response (session cookie #1)
481            //   - response should be: {"counter": 1, "user_id": None}
482            let req_2 = srv.post("/do_something").send();
483            let mut resp_2 = req_2.await.unwrap();
484            let result_2 = resp_2.json::<IndexResponse>().await.unwrap();
485            assert_eq!(
486                result_2,
487                IndexResponse {
488                    user_id: None,
489                    counter: 1
490                }
491            );
492            let cookie_1 = resp_2
493                .cookies()
494                .unwrap()
495                .clone()
496                .into_iter()
497                .find(|c| c.name() == "test-session")
498                .unwrap();
499            assert_eq!(cookie_1.max_age(), Some(session_ttl));
500
501            // Step 3:  GET index, including session cookie #1 in request
502            //   - set-cookie will *not* be in response
503            //   - response should be: {"counter": 1, "user_id": None}
504            let req_3 = srv.get("/").cookie(cookie_1.clone()).send();
505            let mut resp_3 = req_3.await.unwrap();
506            assert!(resp_3.cookies().unwrap().is_empty());
507            let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
508            assert_eq!(
509                result_3,
510                IndexResponse {
511                    user_id: None,
512                    counter: 1
513                }
514            );
515
516            // Step 4: POST again to do_something, including session cookie #1 in request
517            //   - set-cookie will be in response (session cookie #2)
518            //   - updates session state:  {"counter": 2}
519            //   - response should be: {"counter": 2, "user_id": None}
520            let req_4 = srv.post("/do_something").cookie(cookie_1.clone()).send();
521            let mut resp_4 = req_4.await.unwrap();
522            let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
523            assert_eq!(
524                result_4,
525                IndexResponse {
526                    user_id: None,
527                    counter: 2
528                }
529            );
530            let cookie_2 = resp_4
531                .cookies()
532                .unwrap()
533                .clone()
534                .into_iter()
535                .find(|c| c.name() == "test-session")
536                .unwrap();
537            assert_eq!(cookie_2.max_age(), cookie_1.max_age());
538
539            // Step 5: POST to login, including session cookie #2 in request
540            //   - set-cookie actix-session will be in response  (session cookie #3)
541            //   - updates session state: {"counter": 2, "user_id": "ferris"}
542            let req_5 = srv
543                .post("/login")
544                .cookie(cookie_2.clone())
545                .send_json(&json!({"user_id": "ferris"}));
546            let mut resp_5 = req_5.await.unwrap();
547            let cookie_3 = resp_5
548                .cookies()
549                .unwrap()
550                .clone()
551                .into_iter()
552                .find(|c| c.name() == "test-session")
553                .unwrap();
554            assert_ne!(cookie_2.value(), cookie_3.value());
555
556            let result_5 = resp_5.json::<IndexResponse>().await.unwrap();
557            assert_eq!(
558                result_5,
559                IndexResponse {
560                    user_id: Some("ferris".into()),
561                    counter: 2
562                }
563            );
564
565            // Step 6: GET index, including session cookie #3 in request
566            //   - response should be: {"counter": 2, "user_id": "ferris"}
567            let req_6 = srv.get("/").cookie(cookie_3.clone()).send();
568            let mut resp_6 = req_6.await.unwrap();
569            let result_6 = resp_6.json::<IndexResponse>().await.unwrap();
570            assert_eq!(
571                result_6,
572                IndexResponse {
573                    user_id: Some("ferris".into()),
574                    counter: 2
575                }
576            );
577
578            // Step 7: POST again to do_something, including session cookie #3 in request
579            //   - updates session state: {"counter": 3, "user_id": "ferris"}
580            //   - response should be: {"counter": 3, "user_id": "ferris"}
581            let req_7 = srv.post("/do_something").cookie(cookie_3.clone()).send();
582            let mut resp_7 = req_7.await.unwrap();
583            let result_7 = resp_7.json::<IndexResponse>().await.unwrap();
584            assert_eq!(
585                result_7,
586                IndexResponse {
587                    user_id: Some("ferris".into()),
588                    counter: 3
589                }
590            );
591
592            // Step 8: GET index, including session cookie #2 in request
593            // If invalidation is supported, no state will be found associated to this session.
594            // If invalidation is not supported, the old state will still be retrieved.
595            let req_8 = srv.get("/").cookie(cookie_2.clone()).send();
596            let mut resp_8 = req_8.await.unwrap();
597            if is_invalidation_supported {
598                assert!(resp_8.cookies().unwrap().is_empty());
599                let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
600                assert_eq!(
601                    result_8,
602                    IndexResponse {
603                        user_id: None,
604                        counter: 0
605                    }
606                );
607            } else {
608                let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
609                assert_eq!(
610                    result_8,
611                    IndexResponse {
612                        user_id: None,
613                        counter: 2
614                    }
615                );
616            }
617
618            // Step 9: POST to logout, including session cookie #3
619            //   - set-cookie actix-session will be in response with session cookie #3
620            //     invalidation logic
621            let req_9 = srv.post("/logout").cookie(cookie_3.clone()).send();
622            let resp_9 = req_9.await.unwrap();
623            let cookie_3 = resp_9
624                .cookies()
625                .unwrap()
626                .clone()
627                .into_iter()
628                .find(|c| c.name() == "test-session")
629                .unwrap();
630            assert_eq!(0, cookie_3.max_age().map(|t| t.whole_seconds()).unwrap());
631            assert_eq!("/", cookie_3.path().unwrap());
632
633            // Step 10: GET index, including session cookie #3 in request
634            //   - set-cookie actix-session should NOT be in response if invalidation is supported
635            //   - response should be: {"counter": 0, "user_id": None}
636            let req_10 = srv.get("/").cookie(cookie_3.clone()).send();
637            let mut resp_10 = req_10.await.unwrap();
638            if is_invalidation_supported {
639                assert!(resp_10.cookies().unwrap().is_empty());
640            }
641            let result_10 = resp_10.json::<IndexResponse>().await.unwrap();
642            assert_eq!(
643                result_10,
644                IndexResponse {
645                    user_id: None,
646                    counter: 0
647                }
648            );
649        }
650
651        #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
652        pub struct IndexResponse {
653            user_id: Option<String>,
654            counter: i32,
655        }
656
657        async fn index(session: Session) -> Result<HttpResponse> {
658            let user_id: Option<String> = session.get::<String>("user_id").unwrap();
659            let counter: i32 = session
660                .get::<i32>("counter")
661                .unwrap_or(Some(0))
662                .unwrap_or(0);
663
664            Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
665        }
666
667        async fn do_something(session: Session) -> Result<HttpResponse> {
668            let user_id: Option<String> = session.get::<String>("user_id").unwrap();
669            let counter: i32 = session
670                .get::<i32>("counter")
671                .unwrap_or(Some(0))
672                .map_or(1, |inner| inner + 1);
673            session.insert("counter", counter)?;
674
675            Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
676        }
677
678        async fn count(session: Session) -> Result<HttpResponse> {
679            let user_id: Option<String> = session.get::<String>("user_id").unwrap();
680            let counter: i32 = session.get::<i32>("counter").unwrap().unwrap();
681
682            Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
683        }
684
685        #[derive(Deserialize)]
686        struct Identity {
687            user_id: String,
688        }
689
690        async fn login(user_id: web::Json<Identity>, session: Session) -> Result<HttpResponse> {
691            let id = user_id.into_inner().user_id;
692            session.insert("user_id", &id)?;
693            session.renew();
694
695            let counter: i32 = session
696                .get::<i32>("counter")
697                .unwrap_or(Some(0))
698                .unwrap_or(0);
699
700            Ok(HttpResponse::Ok().json(&IndexResponse {
701                user_id: Some(id),
702                counter,
703            }))
704        }
705
706        async fn logout(session: Session) -> Result<HttpResponse> {
707            let id: Option<String> = session.get("user_id")?;
708
709            let body = if let Some(id) = id {
710                session.purge();
711                format!("Logged out: {id}")
712            } else {
713                "Could not log out anonymous user".to_owned()
714            };
715
716            Ok(HttpResponse::Ok().body(body))
717        }
718
719        trait ServiceResponseExt {
720            fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>>;
721        }
722
723        impl ServiceResponseExt for ServiceResponse {
724            fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>> {
725                self.response().cookies().find(|c| c.name() == cookie_name)
726            }
727        }
728    }
729}