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