Skip to main content

ntex_identity/
lib.rs

1//! Request identity service for ntex applications.
2//!
3//! [**IdentityService**](struct.IdentityService.html) middleware can be
4//! used with different policies types to store identity information.
5//!
6//! By default, only cookie identity policy is implemented. Other backend
7//! implementations can be added separately.
8//!
9//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html)
10//! uses cookies as identity storage.
11//!
12//! To access current request identity
13//! [**Identity**](struct.Identity.html) extractor should be used.
14//!
15//! ```rust
16//! use ntex::web;
17//! use ntex_identity::{Identity, CookieIdentityPolicy, IdentityService};
18//!
19//! async fn index(id: Identity) -> String {
20//!     // access request identity
21//!     if let Some(id) = id.identity() {
22//!         format!("Welcome! {}", id)
23//!     } else {
24//!         "Welcome Anonymous!".to_owned()
25//!     }
26//! }
27//!
28//! async fn login(id: Identity) -> web::HttpResponse {
29//!     id.remember("User1".to_owned()); // <- remember identity
30//!     web::HttpResponse::Ok().finish()
31//! }
32//!
33//! async fn logout(id: Identity) -> web::HttpResponse {
34//!     id.forget();                      // <- remove identity
35//!     web::HttpResponse::Ok().finish()
36//! }
37//!
38//! let app = web::App::new().wrap(IdentityService::new(
39//!     // <- create identity middleware
40//!     CookieIdentityPolicy::new(&[0; 32])    // <- create cookie identity policy
41//!           .name("auth-cookie")
42//!           .secure(false)))
43//!     .service(web::resource("/index.html").to(index))
44//!     .service(web::resource("/login.html").to(login))
45//!     .service(web::resource("/logout.html").to(logout));
46//! ```
47use std::{convert::Infallible, future::Future, rc::Rc, time::SystemTime};
48
49use cookie::{Cookie, CookieJar, Key, SameSite};
50use derive_more::{Display, From};
51use futures::future::{Ready, ok};
52use serde::{Deserialize, Serialize};
53use time::Duration;
54
55use ntex::http::header::{self, HeaderValue};
56use ntex::http::{HttpMessage, Payload, error::HttpError};
57use ntex::service::{Middleware, Service, ServiceCtx};
58use ntex::util::Extensions;
59use ntex::web::{
60    DefaultError, ErrorRenderer, FromRequest, HttpRequest, WebRequest, WebResponse,
61    WebResponseError,
62};
63
64/// The extractor type to obtain your identity from a request.
65///
66/// ```rust
67/// use ntex::web::{self, Error};
68/// use ntex_identity::Identity;
69///
70/// fn index(id: Identity) -> Result<String, web::Error> {
71///     // access request identity
72///     if let Some(id) = id.identity() {
73///         Ok(format!("Welcome! {}", id))
74///     } else {
75///         Ok("Welcome Anonymous!".to_owned())
76///     }
77/// }
78///
79/// fn login(id: Identity) -> web::HttpResponse {
80///     id.remember("User1".to_owned()); // <- remember identity
81///     web::HttpResponse::Ok().finish()
82/// }
83///
84/// fn logout(id: Identity) -> web::HttpResponse {
85///     id.forget(); // <- remove identity
86///     web::HttpResponse::Ok().finish()
87/// }
88/// # fn main() {}
89/// ```
90#[derive(Clone)]
91pub struct Identity(HttpRequest);
92
93impl Identity {
94    /// Return the claimed identity of the user associated request or
95    /// ``None`` if no identity can be found associated with the request.
96    pub fn identity(&self) -> Option<String> {
97        Identity::get_identity(&self.0.extensions())
98    }
99
100    /// Remember identity.
101    pub fn remember(&self, identity: String) {
102        if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
103            id.id = Some(identity);
104            id.changed = true;
105        }
106    }
107
108    /// This method is used to 'forget' the current identity on subsequent
109    /// requests.
110    pub fn forget(&self) {
111        if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
112            id.id = None;
113            id.changed = true;
114        }
115    }
116
117    fn get_identity(extensions: &Extensions) -> Option<String> {
118        if let Some(id) = extensions.get::<IdentityItem>() { id.id.clone() } else { None }
119    }
120}
121
122struct IdentityItem {
123    id: Option<String>,
124    changed: bool,
125}
126
127/// Helper trait that allows to get Identity.
128///
129/// It could be used in middleware but identity policy must be set before any other middleware that needs identity
130/// RequestIdentity is implemented both for `ServiceRequest` and `HttpRequest`.
131pub trait RequestIdentity {
132    fn get_identity(&self) -> Option<String>;
133}
134
135impl<T> RequestIdentity for T
136where
137    T: HttpMessage,
138{
139    fn get_identity(&self) -> Option<String> {
140        Identity::get_identity(&self.message_extensions())
141    }
142}
143
144/// Extractor implementation for Identity type.
145///
146/// ```rust
147/// use ntex_identity::Identity;
148///
149/// fn index(id: Identity) -> String {
150///     // access request identity
151///     if let Some(id) = id.identity() {
152///         format!("Welcome! {}", id)
153///     } else {
154///         "Welcome Anonymous!".to_owned()
155///     }
156/// }
157/// # fn main() {}
158/// ```
159impl<Err: ErrorRenderer> FromRequest<Err> for Identity {
160    type Error = Infallible;
161
162    #[inline]
163    async fn from_request(req: &HttpRequest, _: &mut Payload) -> Result<Identity, Infallible> {
164        Ok(Identity(req.clone()))
165    }
166}
167
168#[allow(clippy::wrong_self_convention)]
169/// Identity policy definition.
170pub trait IdentityPolicy<Err>: Sized + 'static {
171    /// The return type of the middleware
172    type Future: Future<Output = Result<Option<String>, Self::Error>>;
173
174    /// The return type of the middleware
175    type ResponseFuture: Future<Output = Result<(), Self::Error>>;
176
177    /// The error type of the policy
178    type Error;
179
180    /// Parse the session from request and load data from a service identity.
181    fn from_request(&self, request: &mut WebRequest<Err>) -> Self::Future;
182
183    /// Write changes to response
184    fn to_response(
185        &self,
186        identity: Option<String>,
187        changed: bool,
188        response: &mut WebResponse,
189    ) -> Self::ResponseFuture;
190}
191
192/// Request identity middleware
193///
194/// ```rust
195/// use ntex::web::App;
196/// use ntex_identity::{CookieIdentityPolicy, IdentityService};
197///
198/// let app = App::new().wrap(IdentityService::new(
199///     // <- create identity middleware
200///     CookieIdentityPolicy::new(&[0; 32])    // <- create cookie session backend
201///           .name("auth-cookie")
202///           .secure(false),
203/// ));
204/// ```
205pub struct IdentityService<T> {
206    backend: Rc<T>,
207}
208
209impl<T> IdentityService<T> {
210    /// Create new identity service with specified backend.
211    pub fn new(backend: T) -> Self {
212        IdentityService { backend: Rc::new(backend) }
213    }
214}
215
216impl<S, C, T> Middleware<S, C> for IdentityService<T> {
217    type Service = IdentityServiceMiddleware<S, T>;
218
219    fn create(&self, service: S, _: C) -> Self::Service {
220        IdentityServiceMiddleware { service, backend: self.backend.clone() }
221    }
222}
223
224#[doc(hidden)]
225pub struct IdentityServiceMiddleware<S, T> {
226    backend: Rc<T>,
227    service: S,
228}
229
230impl<S: Clone, T> Clone for IdentityServiceMiddleware<S, T> {
231    fn clone(&self) -> Self {
232        Self { backend: self.backend.clone(), service: self.service.clone() }
233    }
234}
235
236impl<S, T, Err> Service<WebRequest<Err>> for IdentityServiceMiddleware<S, T>
237where
238    S: Service<WebRequest<Err>, Response = WebResponse> + 'static,
239    T: IdentityPolicy<Err>,
240    Err: ErrorRenderer,
241    Err::Container: From<S::Error>,
242    Err::Container: From<T::Error>,
243{
244    type Response = WebResponse;
245    type Error = S::Error;
246
247    ntex::forward_ready!(service);
248    ntex::forward_shutdown!(service);
249
250    async fn call(
251        &self,
252        mut req: WebRequest<Err>,
253        ctx: ServiceCtx<'_, Self>,
254    ) -> Result<Self::Response, Self::Error> {
255        match self.backend.from_request(&mut req).await {
256            Ok(id) => {
257                req.extensions_mut().insert(IdentityItem { id, changed: false });
258
259                // https://github.com/actix/actix-web/issues/1263
260                let mut res = ctx.call(&self.service, req).await?;
261                let id = res.request().extensions_mut().remove::<IdentityItem>();
262
263                if let Some(id) = id {
264                    match self.backend.to_response(id.id, id.changed, &mut res).await {
265                        Ok(_) => Ok(res),
266                        Err(e) => Ok(WebResponse::error_response::<Err, _>(res, e)),
267                    }
268                } else {
269                    Ok(res)
270                }
271            }
272            Err(err) => Ok(req.error_response(err)),
273        }
274    }
275}
276
277struct CookieIdentityInner {
278    key: Key,
279    key_v2: Key,
280    name: String,
281    path: String,
282    domain: Option<String>,
283    secure: bool,
284    max_age: Option<Duration>,
285    same_site: Option<SameSite>,
286    visit_deadline: Option<Duration>,
287    login_deadline: Option<Duration>,
288}
289
290#[derive(Deserialize, Serialize, Debug)]
291struct CookieValue {
292    identity: String,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    login_timestamp: Option<SystemTime>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    visit_timestamp: Option<SystemTime>,
297}
298
299#[derive(Debug)]
300struct CookieIdentityExtention {
301    login_timestamp: Option<SystemTime>,
302}
303
304impl CookieIdentityInner {
305    fn new(key: &[u8]) -> CookieIdentityInner {
306        let key_v2: Vec<u8> = key.iter().chain([1, 0, 0, 0].iter()).cloned().collect();
307        CookieIdentityInner {
308            key: Key::derive_from(key),
309            key_v2: Key::derive_from(&key_v2),
310            name: "ntex-identity".to_owned(),
311            path: "/".to_owned(),
312            domain: None,
313            secure: true,
314            max_age: None,
315            same_site: None,
316            visit_deadline: None,
317            login_deadline: None,
318        }
319    }
320
321    fn set_cookie(
322        &self,
323        resp: &mut WebResponse,
324        value: Option<CookieValue>,
325    ) -> Result<(), CookieIdentityPolicyError> {
326        let add_cookie = value.is_some();
327        let val = value.map(|val| {
328            if !self.legacy_supported() {
329                serde_json::to_string(&val)
330            } else {
331                Ok(val.identity)
332            }
333        });
334        let mut cookie =
335            Cookie::new(self.name.clone(), val.unwrap_or_else(|| Ok(String::new()))?);
336        cookie.set_path(self.path.clone());
337        cookie.set_secure(self.secure);
338        cookie.set_http_only(true);
339        cookie.set_max_age(self.max_age);
340
341        if let Some(ref domain) = self.domain {
342            cookie.set_domain(domain.clone());
343        }
344
345        if let Some(same_site) = self.same_site {
346            cookie.set_same_site(same_site);
347        }
348
349        let mut jar = CookieJar::new();
350        let key = if self.legacy_supported() { &self.key } else { &self.key_v2 };
351        if add_cookie {
352            jar.private_mut(key).add(cookie);
353        } else {
354            jar.add_original(cookie.clone());
355            jar.private_mut(key).remove(cookie);
356        }
357        for cookie in jar.delta() {
358            let val = HeaderValue::from_str(&cookie.to_string()).map_err(HttpError::from)?;
359            resp.headers_mut().append(header::SET_COOKIE, val);
360        }
361        Ok(())
362    }
363
364    fn load<Err>(&self, req: &WebRequest<Err>) -> Option<CookieValue> {
365        let cookie = req.cookie(&self.name)?;
366        let mut jar = CookieJar::new();
367        jar.add_original(cookie.clone());
368        let res = if self.legacy_supported() {
369            jar.private(&self.key).get(&self.name).map(|n| CookieValue {
370                identity: n.value().to_string(),
371                login_timestamp: None,
372                visit_timestamp: None,
373            })
374        } else {
375            None
376        };
377        res.or_else(|| jar.private(&self.key_v2).get(&self.name).and_then(|c| self.parse(c)))
378    }
379
380    fn parse(&self, cookie: Cookie) -> Option<CookieValue> {
381        let value: CookieValue = serde_json::from_str(cookie.value()).ok()?;
382        let now = SystemTime::now();
383        if let Some(visit_deadline) = self.visit_deadline
384            && now.duration_since(value.visit_timestamp?).ok()? > visit_deadline
385        {
386            return None;
387        }
388        if let Some(login_deadline) = self.login_deadline
389            && now.duration_since(value.login_timestamp?).ok()? > login_deadline
390        {
391            return None;
392        }
393        Some(value)
394    }
395
396    fn legacy_supported(&self) -> bool {
397        self.visit_deadline.is_none() && self.login_deadline.is_none()
398    }
399
400    fn always_update_cookie(&self) -> bool {
401        self.visit_deadline.is_some()
402    }
403
404    fn requires_oob_data(&self) -> bool {
405        self.login_deadline.is_some()
406    }
407}
408
409/// Use cookies for request identity storage.
410///
411/// The constructors take a key as an argument.
412/// This is the private key for cookie - when this value is changed,
413/// all identities are lost. The constructors will panic if the key is less
414/// than 32 bytes in length.
415///
416/// # Example
417///
418/// ```rust
419/// use ntex::web::App;
420/// use ntex_identity::{CookieIdentityPolicy, IdentityService};
421///
422/// let app = App::new().wrap(IdentityService::new(
423///     // <- create identity middleware
424///     CookieIdentityPolicy::new(&[0; 32])  // <- construct cookie policy
425///            .domain("www.rust-lang.org")
426///            .name("ntex-auth")
427///            .path("/")
428///            .secure(true),
429/// ));
430/// ```
431pub struct CookieIdentityPolicy(Rc<CookieIdentityInner>);
432
433#[derive(Debug, Display, From)]
434pub enum CookieIdentityPolicyError {
435    Http(HttpError),
436    Json(serde_json::error::Error),
437}
438
439impl WebResponseError<DefaultError> for CookieIdentityPolicyError {}
440
441impl CookieIdentityPolicy {
442    /// Construct new `CookieIdentityPolicy` instance.
443    ///
444    /// Panics if key length is less than 32 bytes.
445    pub fn new(key: &[u8]) -> Self {
446        CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key)))
447    }
448
449    /// Sets the `path` field in the session cookie being built.
450    pub fn path<S: Into<String>>(mut self, value: S) -> Self {
451        Rc::get_mut(&mut self.0).unwrap().path = value.into();
452        self
453    }
454
455    /// Sets the `name` field in the session cookie being built.
456    pub fn name<S: Into<String>>(mut self, value: S) -> Self {
457        Rc::get_mut(&mut self.0).unwrap().name = value.into();
458        self
459    }
460
461    /// Sets the `domain` field in the session cookie being built.
462    pub fn domain<S: Into<String>>(mut self, value: S) -> Self {
463        Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into());
464        self
465    }
466
467    /// Sets the `secure` field in the session cookie being built.
468    ///
469    /// If the `secure` field is set, a cookie will only be transmitted when the
470    /// connection is secure - i.e. `https`
471    pub fn secure(mut self, value: bool) -> Self {
472        Rc::get_mut(&mut self.0).unwrap().secure = value;
473        self
474    }
475
476    /// Sets the `max-age` field in the session cookie being built with given number of seconds.
477    pub fn max_age(self, seconds: i64) -> Self {
478        self.max_age_time(Duration::seconds(seconds))
479    }
480
481    /// Sets the `max-age` field in the session cookie being built with `chrono::Duration`.
482    pub fn max_age_time(mut self, value: Duration) -> Self {
483        Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
484        self
485    }
486
487    /// Sets the `same_site` field in the session cookie being built.
488    pub fn same_site(mut self, same_site: SameSite) -> Self {
489        Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site);
490        self
491    }
492
493    /// Accepts only users whose cookie has been seen before the given deadline
494    ///
495    /// By default visit deadline is disabled.
496    pub fn visit_deadline(mut self, value: Duration) -> Self {
497        Rc::get_mut(&mut self.0).unwrap().visit_deadline = Some(value);
498        self
499    }
500
501    /// Accepts only users which has been authenticated before the given deadline
502    ///
503    /// By default login deadline is disabled.
504    pub fn login_deadline(mut self, value: Duration) -> Self {
505        Rc::get_mut(&mut self.0).unwrap().login_deadline = Some(value);
506        self
507    }
508}
509
510impl<Err: ErrorRenderer> IdentityPolicy<Err> for CookieIdentityPolicy {
511    type Error = CookieIdentityPolicyError;
512    type Future = Ready<Result<Option<String>, CookieIdentityPolicyError>>;
513    type ResponseFuture = Ready<Result<(), CookieIdentityPolicyError>>;
514
515    fn from_request(&self, req: &mut WebRequest<Err>) -> Self::Future {
516        ok(self.0.load(req).map(|CookieValue { identity, login_timestamp, .. }| {
517            if self.0.requires_oob_data() {
518                req.extensions_mut().insert(CookieIdentityExtention { login_timestamp });
519            }
520            identity
521        }))
522    }
523
524    fn to_response(
525        &self,
526        id: Option<String>,
527        changed: bool,
528        res: &mut WebResponse,
529    ) -> Self::ResponseFuture {
530        let _ = if changed {
531            let login_timestamp = SystemTime::now();
532            self.0.set_cookie(
533                res,
534                id.map(|identity| CookieValue {
535                    identity,
536                    login_timestamp: self.0.login_deadline.map(|_| login_timestamp),
537                    visit_timestamp: self.0.visit_deadline.map(|_| login_timestamp),
538                }),
539            )
540        } else if self.0.always_update_cookie() && id.is_some() {
541            let visit_timestamp = SystemTime::now();
542            let login_timestamp = if self.0.requires_oob_data() {
543                let CookieIdentityExtention { login_timestamp: lt } =
544                    res.request().extensions_mut().remove().unwrap();
545                lt
546            } else {
547                None
548            };
549            self.0.set_cookie(
550                res,
551                Some(CookieValue {
552                    identity: id.unwrap(),
553                    login_timestamp,
554                    visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp),
555                }),
556            )
557        } else {
558            Ok(())
559        };
560        ok(())
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use std::borrow::Borrow;
567
568    use super::*;
569    use ntex::web::test::{self, TestRequest};
570    use ntex::web::{self, App, Error, HttpResponse, error};
571    use ntex::{http::StatusCode, service::Pipeline, service::fn_service, time};
572
573    const COOKIE_KEY_MASTER: [u8; 32] = [0; 32];
574    const COOKIE_NAME: &str = "ntex_auth";
575    const COOKIE_LOGIN: &str = "test";
576
577    #[ntex::test]
578    async fn test_identity() {
579        let srv = test::init_service(
580            App::new()
581                .wrap(IdentityService::new(
582                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
583                        .domain("www.rust-lang.org")
584                        .name(COOKIE_NAME)
585                        .path("/")
586                        .secure(true),
587                ))
588                .service(web::resource("/index").to(|id: Identity| async move {
589                    if id.identity().is_some() {
590                        HttpResponse::Created()
591                    } else {
592                        HttpResponse::Ok()
593                    }
594                }))
595                .service(web::resource("/login").to(|id: Identity| async move {
596                    id.remember(COOKIE_LOGIN.to_string());
597                    HttpResponse::Ok()
598                }))
599                .service(web::resource("/logout").to(|id: Identity| async move {
600                    if id.identity().is_some() {
601                        id.forget();
602                        HttpResponse::Ok()
603                    } else {
604                        HttpResponse::BadRequest()
605                    }
606                })),
607        )
608        .await;
609
610        let resp = test::call_service(&srv, TestRequest::with_uri("/index").to_request()).await;
611        assert_eq!(resp.status(), StatusCode::OK);
612
613        let resp = test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
614        assert_eq!(resp.status(), StatusCode::OK);
615        let c = resp.response().cookies().next().unwrap().into_owned();
616
617        let resp = test::call_service(
618            &srv,
619            TestRequest::with_uri("/index").cookie(c.clone()).to_request(),
620        )
621        .await;
622        assert_eq!(resp.status(), StatusCode::CREATED);
623
624        let resp = test::call_service(
625            &srv,
626            TestRequest::with_uri("/logout").cookie(c.clone()).to_request(),
627        )
628        .await;
629        assert_eq!(resp.status(), StatusCode::OK);
630        assert!(resp.headers().contains_key(header::SET_COOKIE))
631    }
632
633    #[ntex::test]
634    async fn test_identity_max_age_time() {
635        let duration = Duration::days(1);
636        let srv = test::init_service(
637            App::new()
638                .wrap(IdentityService::new(
639                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
640                        .domain("www.rust-lang.org")
641                        .name(COOKIE_NAME)
642                        .path("/")
643                        .max_age_time(duration)
644                        .secure(true),
645                ))
646                .service(web::resource("/login").to(|id: Identity| async move {
647                    id.remember("test".to_string());
648                    HttpResponse::Ok()
649                })),
650        )
651        .await;
652        let resp = test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
653        assert_eq!(resp.status(), StatusCode::OK);
654        assert!(resp.headers().contains_key(header::SET_COOKIE));
655        let c = resp.response().cookies().next().unwrap().to_owned();
656        assert_eq!(duration, c.max_age().unwrap());
657    }
658
659    #[ntex::test]
660    async fn test_identity_max_age() {
661        let seconds = 60;
662        let srv = test::init_service(
663            App::new()
664                .wrap(IdentityService::new(
665                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
666                        .domain("www.rust-lang.org")
667                        .name(COOKIE_NAME)
668                        .path("/")
669                        .max_age(seconds)
670                        .secure(true),
671                ))
672                .service(web::resource("/login").to(|id: Identity| async move {
673                    id.remember("test".to_string());
674                    HttpResponse::Ok()
675                })),
676        )
677        .await;
678        let resp = test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
679        assert_eq!(resp.status(), StatusCode::OK);
680        assert!(resp.headers().contains_key(header::SET_COOKIE));
681        let c = resp.response().cookies().next().unwrap().to_owned();
682        assert_eq!(Duration::seconds(seconds), c.max_age().unwrap());
683    }
684
685    async fn create_identity_server<
686        F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static,
687    >(
688        f: F,
689    ) -> Pipeline<
690        impl ntex::service::Service<ntex::http::Request, Response = WebResponse, Error = Error>,
691    > {
692        test::init_service(
693            App::new()
694                .wrap(IdentityService::new(f(CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
695                    .secure(false)
696                    .name(COOKIE_NAME))))
697                .service(web::resource("/").to(|id: Identity| async move {
698                    let identity = id.identity();
699                    if identity.is_none() {
700                        id.remember(COOKIE_LOGIN.to_string())
701                    }
702                    web::types::Json(identity)
703                })),
704        )
705        .await
706    }
707
708    fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> {
709        let mut jar = CookieJar::new();
710        jar.private_mut(&Key::derive_from(&COOKIE_KEY_MASTER))
711            .add(Cookie::new(COOKIE_NAME, identity));
712        jar.get(COOKIE_NAME).unwrap().clone()
713    }
714
715    fn login_cookie(
716        identity: &'static str,
717        login_timestamp: Option<SystemTime>,
718        visit_timestamp: Option<SystemTime>,
719    ) -> Cookie<'static> {
720        let mut jar = CookieJar::new();
721        let key: Vec<u8> =
722            COOKIE_KEY_MASTER.iter().chain([1, 0, 0, 0].iter()).copied().collect();
723        jar.private_mut(&Key::derive_from(&key)).add(Cookie::new(
724            COOKIE_NAME,
725            serde_json::to_string(&CookieValue {
726                identity: identity.to_string(),
727                login_timestamp,
728                visit_timestamp,
729            })
730            .unwrap(),
731        ));
732        jar.get(COOKIE_NAME).unwrap().clone()
733    }
734
735    async fn assert_logged_in(response: WebResponse, identity: Option<&str>) {
736        let bytes = test::read_body(response).await;
737        let resp: Option<String> = serde_json::from_slice(&bytes[..]).unwrap();
738        assert_eq!(resp.as_ref().map(|s| s.borrow()), identity);
739    }
740
741    fn assert_legacy_login_cookie(response: &mut WebResponse, identity: &str) {
742        let mut cookies = CookieJar::new();
743        for cookie in response.headers().get_all(header::SET_COOKIE) {
744            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
745        }
746        let cookie =
747            cookies.private(&Key::derive_from(&COOKIE_KEY_MASTER)).get(COOKIE_NAME).unwrap();
748        assert_eq!(cookie.value(), identity);
749    }
750
751    enum LoginTimestampCheck {
752        Incorrect,
753        New,
754        Old(SystemTime),
755    }
756
757    enum VisitTimeStampCheck {
758        NoTimestamp,
759        NewTimestamp,
760    }
761
762    fn assert_login_cookie(
763        response: &mut WebResponse,
764        identity: &str,
765        login_timestamp: LoginTimestampCheck,
766        visit_timestamp: VisitTimeStampCheck,
767    ) {
768        let mut cookies = CookieJar::new();
769        for cookie in response.headers().get_all(header::SET_COOKIE) {
770            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
771        }
772        let key: Vec<u8> =
773            COOKIE_KEY_MASTER.iter().chain([1, 0, 0, 0].iter()).copied().collect();
774        let cookie = cookies.private(&Key::derive_from(&key)).get(COOKIE_NAME).unwrap();
775        let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap();
776        assert_eq!(cv.identity, identity);
777        let now = SystemTime::now();
778        let t30sec_ago = now - Duration::seconds(30);
779        match login_timestamp {
780            LoginTimestampCheck::Incorrect => assert_eq!(cv.login_timestamp, None),
781            LoginTimestampCheck::New => assert!(
782                t30sec_ago <= cv.login_timestamp.unwrap() && cv.login_timestamp.unwrap() <= now
783            ),
784            LoginTimestampCheck::Old(old_timestamp) => {
785                assert_eq!(cv.login_timestamp, Some(old_timestamp))
786            }
787        }
788        match visit_timestamp {
789            VisitTimeStampCheck::NoTimestamp => assert_eq!(cv.visit_timestamp, None),
790            VisitTimeStampCheck::NewTimestamp => assert!(
791                t30sec_ago <= cv.visit_timestamp.unwrap() && cv.visit_timestamp.unwrap() <= now
792            ),
793        }
794    }
795
796    fn assert_no_login_cookie(response: &mut WebResponse) {
797        let mut cookies = CookieJar::new();
798        for cookie in response.headers().get_all(header::SET_COOKIE) {
799            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
800        }
801        assert!(cookies.get(COOKIE_NAME).is_none());
802    }
803
804    #[ntex::test]
805    async fn test_identity_legacy_cookie_is_set() {
806        let srv = create_identity_server(|c| c).await;
807        let mut resp = test::call_service(&srv, TestRequest::with_uri("/").to_request()).await;
808        assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN);
809        assert_logged_in(resp, None).await;
810    }
811
812    #[ntex::test]
813    async fn test_identity_legacy_cookie_works() {
814        let srv = create_identity_server(|c| c).await;
815        let cookie = legacy_login_cookie(COOKIE_LOGIN);
816        let mut resp = test::call_service(
817            &srv,
818            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
819        )
820        .await;
821        assert_no_login_cookie(&mut resp);
822        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
823    }
824
825    #[ntex::test]
826    async fn test_identity_legacy_cookie_rejected_if_visit_timestamp_needed() {
827        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
828        let cookie = legacy_login_cookie(COOKIE_LOGIN);
829        let mut resp = test::call_service(
830            &srv,
831            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
832        )
833        .await;
834        assert_login_cookie(
835            &mut resp,
836            COOKIE_LOGIN,
837            LoginTimestampCheck::Incorrect,
838            VisitTimeStampCheck::NewTimestamp,
839        );
840        assert_logged_in(resp, None).await;
841    }
842
843    #[ntex::test]
844    async fn test_identity_legacy_cookie_rejected_if_login_timestamp_needed() {
845        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
846        let cookie = legacy_login_cookie(COOKIE_LOGIN);
847        let mut resp = test::call_service(
848            &srv,
849            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
850        )
851        .await;
852        assert_login_cookie(
853            &mut resp,
854            COOKIE_LOGIN,
855            LoginTimestampCheck::New,
856            VisitTimeStampCheck::NoTimestamp,
857        );
858        assert_logged_in(resp, None).await;
859    }
860
861    #[ntex::test]
862    async fn test_identity_cookie_rejected_if_login_timestamp_needed() {
863        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
864        let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now()));
865        let mut resp = test::call_service(
866            &srv,
867            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
868        )
869        .await;
870        assert_login_cookie(
871            &mut resp,
872            COOKIE_LOGIN,
873            LoginTimestampCheck::New,
874            VisitTimeStampCheck::NoTimestamp,
875        );
876        assert_logged_in(resp, None).await;
877    }
878
879    #[ntex::test]
880    async fn test_identity_cookie_rejected_if_visit_timestamp_needed() {
881        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
882        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
883        let mut resp = test::call_service(
884            &srv,
885            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
886        )
887        .await;
888        assert_login_cookie(
889            &mut resp,
890            COOKIE_LOGIN,
891            LoginTimestampCheck::Incorrect,
892            VisitTimeStampCheck::NewTimestamp,
893        );
894        assert_logged_in(resp, None).await;
895    }
896
897    #[ntex::test]
898    async fn test_identity_cookie_rejected_if_login_timestamp_too_old() {
899        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
900        let cookie =
901            login_cookie(COOKIE_LOGIN, Some(SystemTime::now() - Duration::days(180)), None);
902        let mut resp = test::call_service(
903            &srv,
904            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
905        )
906        .await;
907        assert_login_cookie(
908            &mut resp,
909            COOKIE_LOGIN,
910            LoginTimestampCheck::New,
911            VisitTimeStampCheck::NoTimestamp,
912        );
913        assert_logged_in(resp, None).await;
914    }
915
916    #[ntex::test]
917    async fn test_identity_cookie_rejected_if_visit_timestamp_too_old() {
918        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
919        let cookie =
920            login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now() - Duration::days(180)));
921        let mut resp = test::call_service(
922            &srv,
923            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
924        )
925        .await;
926        assert_login_cookie(
927            &mut resp,
928            COOKIE_LOGIN,
929            LoginTimestampCheck::Incorrect,
930            VisitTimeStampCheck::NewTimestamp,
931        );
932        assert_logged_in(resp, None).await;
933    }
934
935    #[ntex::test]
936    async fn test_identity_cookie_not_updated_on_login_deadline() {
937        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
938        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
939        let mut resp = test::call_service(
940            &srv,
941            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
942        )
943        .await;
944        assert_no_login_cookie(&mut resp);
945        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
946    }
947
948    // https://github.com/actix/actix-web/issues/1263
949    #[ntex::test]
950    async fn test_identity_cookie_updated_on_visit_deadline() {
951        let srv = create_identity_server(|c| {
952            c.visit_deadline(Duration::days(90)).login_deadline(Duration::days(90))
953        })
954        .await;
955        let timestamp = SystemTime::now() - Duration::days(1);
956        let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
957        let mut resp = test::call_service(
958            &srv,
959            TestRequest::with_uri("/").cookie(cookie.clone()).to_request(),
960        )
961        .await;
962        assert_login_cookie(
963            &mut resp,
964            COOKIE_LOGIN,
965            LoginTimestampCheck::Old(timestamp),
966            VisitTimeStampCheck::NewTimestamp,
967        );
968        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
969    }
970
971    #[ntex::test]
972    async fn test_borrowed_mut_error() {
973        use futures::future::{Ready, ok};
974        use ntex::web::{DefaultError, Error};
975
976        struct Ident;
977        impl<Err: ErrorRenderer> IdentityPolicy<Err> for Ident {
978            type Error = Error;
979            type Future = Ready<Result<Option<String>, Error>>;
980            type ResponseFuture = Ready<Result<(), Error>>;
981
982            fn from_request(&self, _: &mut WebRequest<Err>) -> Self::Future {
983                ok(Some("test".to_string()))
984            }
985
986            fn to_response(
987                &self,
988                _: Option<String>,
989                _: bool,
990                _: &mut WebResponse,
991            ) -> Self::ResponseFuture {
992                ok(())
993            }
994        }
995
996        let srv: Pipeline<_> = IdentityServiceMiddleware {
997            backend: Rc::new(Ident),
998            service: fn_service(|_: WebRequest<DefaultError>| async move {
999                time::sleep(time::Seconds(100)).await;
1000                Err::<WebResponse, _>(error::ErrorBadRequest("error"))
1001            }),
1002        }
1003        .into();
1004
1005        let srv2 = srv.clone();
1006        let req = TestRequest::default().to_srv_request();
1007        ntex::rt::spawn(async move {
1008            let _ = srv2.call(req).await;
1009        });
1010        time::sleep(time::Millis(50)).await;
1011
1012        srv.ready().await.expect("srv to be ready");
1013    }
1014}