actix / actix-extras
1
//! Request identity service for Actix 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
//! ```
16
//! use actix_web::*;
17
//! use actix_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) -> HttpResponse {
29
//!     id.remember("User1".to_owned()); // <- remember identity
30
//!     HttpResponse::Ok().finish()
31
//! }
32
//!
33
//! async fn logout(id: Identity) -> HttpResponse {
34
//!     id.forget();                      // <- remove identity
35
//!     HttpResponse::Ok().finish()
36
//! }
37
//!
38
//! fn main() {
39
//!     let app = App::new().wrap(IdentityService::new(
40
//!         // <- create identity middleware
41
//!         CookieIdentityPolicy::new(&[0; 32])    // <- create cookie identity policy
42
//!               .name("auth-cookie")
43
//!               .secure(false)))
44
//!         .service(web::resource("/index.html").to(index))
45
//!         .service(web::resource("/login.html").to(login))
46
//!         .service(web::resource("/logout.html").to(logout));
47
//! }
48
//! ```
49

50
#![deny(rust_2018_idioms)]
51

52
use std::{future::Future, rc::Rc, time::SystemTime};
53

54
use actix_service::{Service, Transform};
55
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
56
use serde::{Deserialize, Serialize};
57
use time::Duration;
58

59
use actix_web::cookie::{Cookie, CookieJar, Key, SameSite};
60
use actix_web::dev::{Extensions, Payload, ServiceRequest, ServiceResponse};
61
use actix_web::error::{Error, Result};
62
use actix_web::http::header::{self, HeaderValue};
63
use actix_web::{FromRequest, HttpMessage, HttpRequest};
64

65
/// The extractor type to obtain your identity from a request.
66
///
67
/// ```rust
68
/// use actix_web::*;
69
/// use actix_identity::Identity;
70
///
71
/// fn index(id: Identity) -> Result<String> {
72
///     // access request identity
73
///     if let Some(id) = id.identity() {
74
///         Ok(format!("Welcome! {}", id))
75
///     } else {
76
///         Ok("Welcome Anonymous!".to_owned())
77
///     }
78
/// }
79
///
80
/// fn login(id: Identity) -> HttpResponse {
81
///     id.remember("User1".to_owned()); // <- remember identity
82
///     HttpResponse::Ok().finish()
83
/// }
84
///
85
/// fn logout(id: Identity) -> HttpResponse {
86
///     id.forget(); // <- remove identity
87
///     HttpResponse::Ok().finish()
88
/// }
89
/// # fn main() {}
90
/// ```
91
#[derive(Clone)]
92
pub struct Identity(HttpRequest);
93

94
impl Identity {
95
    /// Return the claimed identity of the user associated request or
96
    /// ``None`` if no identity can be found associated with the request.
97 1
    pub fn identity(&self) -> Option<String> {
98 1
        Identity::get_identity(&self.0.extensions())
99
    }
100

101
    /// Remember identity.
102 1
    pub fn remember(&self, identity: String) {
103 1
        if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
104 1
            id.id = Some(identity);
105 1
            id.changed = true;
106
        }
107
    }
108

109
    /// This method is used to 'forget' the current identity on subsequent
110
    /// requests.
111 1
    pub fn forget(&self) {
112 1
        if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
113 1
            id.id = None;
114 1
            id.changed = true;
115
        }
116
    }
117

118 1
    fn get_identity(extensions: &Extensions) -> Option<String> {
119 1
        if let Some(id) = extensions.get::<IdentityItem>() {
120 1
            id.id.clone()
121
        } else {
122 0
            None
123
        }
124
    }
125
}
126

127
struct IdentityItem {
128
    id: Option<String>,
129
    changed: bool,
130
}
131

132
/// Helper trait that allows to get Identity.
133
///
134
/// It could be used in middleware but identity policy must be set before any other middleware that needs identity
135
/// RequestIdentity is implemented both for `ServiceRequest` and `HttpRequest`.
136
pub trait RequestIdentity {
137
    fn get_identity(&self) -> Option<String>;
138
}
139

140
impl<T> RequestIdentity for T
141
where
142
    T: HttpMessage,
143
{
144 0
    fn get_identity(&self) -> Option<String> {
145 0
        Identity::get_identity(&self.extensions())
146
    }
147
}
148

149
/// Extractor implementation for Identity type.
150
///
151
/// ```rust
152
/// # use actix_web::*;
153
/// use actix_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
/// ```
165
impl FromRequest for Identity {
166
    type Config = ();
167
    type Error = Error;
168
    type Future = Ready<Result<Identity, Error>>;
169

170
    #[inline]
171 1
    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
172 1
        ok(Identity(req.clone()))
173
    }
174
}
175

176
/// Identity policy definition.
177
pub trait IdentityPolicy: Sized + 'static {
178
    /// The return type of the middleware
179
    type Future: Future<Output = Result<Option<String>, Error>>;
180

181
    /// The return type of the middleware
182
    type ResponseFuture: Future<Output = Result<(), Error>>;
183

184
    /// Parse the session from request and load data from a service identity.
185
    fn from_request(&self, request: &mut ServiceRequest) -> Self::Future;
186

187
    /// Write changes to response
188
    fn to_response<B>(
189
        &self,
190
        identity: Option<String>,
191
        changed: bool,
192
        response: &mut ServiceResponse<B>,
193
    ) -> Self::ResponseFuture;
194
}
195

196
/// Request identity middleware
197
///
198
/// ```rust
199
/// use actix_web::App;
200
/// use actix_identity::{CookieIdentityPolicy, IdentityService};
201
///
202
/// let app = App::new().wrap(IdentityService::new(
203
///     // <- create identity middleware
204
///     CookieIdentityPolicy::new(&[0; 32])    // <- create cookie session backend
205
///           .name("auth-cookie")
206
///           .secure(false),
207
/// ));
208
/// ```
209
pub struct IdentityService<T> {
210
    backend: Rc<T>,
211
}
212

213
impl<T> IdentityService<T> {
214
    /// Create new identity service with specified backend.
215 1
    pub fn new(backend: T) -> Self {
216
        IdentityService {
217 1
            backend: Rc::new(backend),
218
        }
219
    }
220
}
221

222
impl<S, T, B> Transform<S, ServiceRequest> for IdentityService<T>
223
where
224
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
225
    S::Future: 'static,
226
    T: IdentityPolicy,
227
    B: 'static,
228
{
229
    type Response = ServiceResponse<B>;
230
    type Error = Error;
231
    type InitError = ();
232
    type Transform = IdentityServiceMiddleware<S, T>;
233
    type Future = Ready<Result<Self::Transform, Self::InitError>>;
234

235 1
    fn new_transform(&self, service: S) -> Self::Future {
236 1
        ok(IdentityServiceMiddleware {
237 1
            backend: self.backend.clone(),
238 1
            service: Rc::new(service),
239
        })
240
    }
241
}
242

243
#[doc(hidden)]
244
pub struct IdentityServiceMiddleware<S, T> {
245
    service: Rc<S>,
246
    backend: Rc<T>,
247
}
248

249
impl<S, T> Clone for IdentityServiceMiddleware<S, T> {
250 1
    fn clone(&self) -> Self {
251
        Self {
252 1
            backend: Rc::clone(&self.backend),
253 1
            service: Rc::clone(&self.service),
254
        }
255
    }
256
}
257

258
impl<S, T, B> Service<ServiceRequest> for IdentityServiceMiddleware<S, T>
259
where
260
    B: 'static,
261
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
262
    S::Future: 'static,
263
    T: IdentityPolicy,
264
{
265
    type Response = ServiceResponse<B>;
266
    type Error = Error;
267
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
268

269
    actix_service::forward_ready!(service);
270

271 1
    fn call(&self, mut req: ServiceRequest) -> Self::Future {
272 1
        let srv = Rc::clone(&self.service);
273 1
        let backend = Rc::clone(&self.backend);
274 1
        let fut = self.backend.from_request(&mut req);
275

276 1
        async move {
277 1
            match fut.await {
278 1
                Ok(id) => {
279 1
                    req.extensions_mut()
280 1
                        .insert(IdentityItem { id, changed: false });
281

282 1
                    let mut res = srv.call(req).await?;
283 1
                    let id = res.request().extensions_mut().remove::<IdentityItem>();
284

285 1
                    if let Some(id) = id {
286 1
                        match backend.to_response(id.id, id.changed, &mut res).await {
287 1
                            Ok(_) => Ok(res),
288 0
                            Err(e) => Ok(res.error_response(e)),
289
                        }
290
                    } else {
291 0
                        Ok(res)
292
                    }
293
                }
294 0
                Err(err) => Ok(req.error_response(err)),
295
            }
296
        }
297
        .boxed_local()
298
    }
299
}
300

301
struct CookieIdentityInner {
302
    key: Key,
303
    key_v2: Key,
304
    name: String,
305
    path: String,
306
    domain: Option<String>,
307
    secure: bool,
308
    max_age: Option<Duration>,
309
    http_only: Option<bool>,
310
    same_site: Option<SameSite>,
311
    visit_deadline: Option<Duration>,
312
    login_deadline: Option<Duration>,
313
}
314

315
#[derive(Deserialize, Serialize, Debug)]
316
struct CookieValue {
317
    identity: String,
318

319
    #[serde(skip_serializing_if = "Option::is_none")]
320
    login_timestamp: Option<SystemTime>,
321

322
    #[serde(skip_serializing_if = "Option::is_none")]
323
    visit_timestamp: Option<SystemTime>,
324
}
325

326
#[derive(Debug)]
327
struct CookieIdentityExtension {
328
    login_timestamp: Option<SystemTime>,
329
}
330

331
impl CookieIdentityInner {
332 1
    fn new(key: &[u8]) -> CookieIdentityInner {
333 1
        let key_v2: Vec<u8> = key.iter().chain([1, 0, 0, 0].iter()).cloned().collect();
334
        CookieIdentityInner {
335 1
            key: Key::derive_from(key),
336 1
            key_v2: Key::derive_from(&key_v2),
337 1
            name: "actix-identity".to_owned(),
338 1
            path: "/".to_owned(),
339
            domain: None,
340
            secure: true,
341
            max_age: None,
342
            http_only: None,
343
            same_site: None,
344
            visit_deadline: None,
345
            login_deadline: None,
346
        }
347
    }
348

349 1
    fn set_cookie<B>(
350
        &self,
351
        resp: &mut ServiceResponse<B>,
352
        value: Option<CookieValue>,
353
    ) -> Result<()> {
354 1
        let add_cookie = value.is_some();
355 1
        let val = value.map(|val| {
356 1
            if !self.legacy_supported() {
357 1
                serde_json::to_string(&val)
358
            } else {
359 1
                Ok(val.identity)
360
            }
361
        });
362 1
        let mut cookie =
363 0
            Cookie::new(self.name.clone(), val.unwrap_or_else(|| Ok(String::new()))?);
364 1
        cookie.set_path(self.path.clone());
365 1
        cookie.set_secure(self.secure);
366 1
        cookie.set_http_only(true);
367

368 1
        if let Some(ref domain) = self.domain {
369 1
            cookie.set_domain(domain.clone());
370
        }
371

372 1
        if let Some(max_age) = self.max_age {
373 1
            cookie.set_max_age(max_age);
374
        }
375

376 1
        if let Some(http_only) = self.http_only {
377 1
            cookie.set_http_only(http_only);
378
        }
379

380 1
        if let Some(same_site) = self.same_site {
381 1
            cookie.set_same_site(same_site);
382
        }
383

384 1
        let mut jar = CookieJar::new();
385 1
        let key = if self.legacy_supported() {
386 1
            &self.key
387
        } else {
388 1
            &self.key_v2
389
        };
390 1
        if add_cookie {
391 1
            jar.private(&key).add(cookie);
392
        } else {
393 1
            jar.add_original(cookie.clone());
394 1
            jar.private(&key).remove(cookie);
395
        }
396 1
        for cookie in jar.delta() {
397 1
            let val = HeaderValue::from_str(&cookie.to_string())?;
398 1
            resp.headers_mut().append(header::SET_COOKIE, val);
399
        }
400 1
        Ok(())
401
    }
402

403 1
    fn load(&self, req: &ServiceRequest) -> Option<CookieValue> {
404 1
        let cookie = req.cookie(&self.name)?;
405 1
        let mut jar = CookieJar::new();
406 1
        jar.add_original(cookie.clone());
407 1
        let res = if self.legacy_supported() {
408 1
            jar.private(&self.key).get(&self.name).map(|n| CookieValue {
409 1
                identity: n.value().to_string(),
410 1
                login_timestamp: None,
411 1
                visit_timestamp: None,
412
            })
413
        } else {
414 1
            None
415
        };
416 1
        res.or_else(|| {
417 1
            jar.private(&self.key_v2)
418 1
                .get(&self.name)
419 1
                .and_then(|c| self.parse(c))
420
        })
421
    }
422

423 1
    fn parse(&self, cookie: Cookie<'_>) -> Option<CookieValue> {
424 1
        let value: CookieValue = serde_json::from_str(cookie.value()).ok()?;
425 1
        let now = SystemTime::now();
426 1
        if let Some(visit_deadline) = self.visit_deadline {
427 1
            if now.duration_since(value.visit_timestamp?).ok()? > visit_deadline {
428 1
                return None;
429
            }
430
        }
431 1
        if let Some(login_deadline) = self.login_deadline {
432 1
            if now.duration_since(value.login_timestamp?).ok()? > login_deadline {
433 1
                return None;
434
            }
435
        }
436 1
        Some(value)
437
    }
438

439 1
    fn legacy_supported(&self) -> bool {
440 1
        self.visit_deadline.is_none() && self.login_deadline.is_none()
441
    }
442

443 1
    fn always_update_cookie(&self) -> bool {
444 1
        self.visit_deadline.is_some()
445
    }
446

447 1
    fn requires_oob_data(&self) -> bool {
448 1
        self.login_deadline.is_some()
449
    }
450
}
451

452
/// Use cookies for request identity storage.
453
///
454
/// The constructors take a key as an argument.
455
/// This is the private key for cookie - when this value is changed,
456
/// all identities are lost. The constructors will panic if the key is less
457
/// than 32 bytes in length.
458
///
459
/// # Example
460
///
461
/// ```rust
462
/// use actix_web::App;
463
/// use actix_identity::{CookieIdentityPolicy, IdentityService};
464
///
465
/// let app = App::new().wrap(IdentityService::new(
466
///     // <- create identity middleware
467
///     CookieIdentityPolicy::new(&[0; 32])  // <- construct cookie policy
468
///            .domain("www.rust-lang.org")
469
///            .name("actix_auth")
470
///            .path("/")
471
///            .secure(true),
472
/// ));
473
/// ```
474
pub struct CookieIdentityPolicy(Rc<CookieIdentityInner>);
475

476
impl CookieIdentityPolicy {
477
    /// Construct new `CookieIdentityPolicy` instance.
478
    ///
479
    /// Panics if key length is less than 32 bytes.
480 1
    pub fn new(key: &[u8]) -> CookieIdentityPolicy {
481 1
        CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key)))
482
    }
483

484
    /// Sets the `path` field in the session cookie being built.
485 1
    pub fn path<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
486 1
        Rc::get_mut(&mut self.0).unwrap().path = value.into();
487 1
        self
488
    }
489

490
    /// Sets the `name` field in the session cookie being built.
491 1
    pub fn name<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
492 1
        Rc::get_mut(&mut self.0).unwrap().name = value.into();
493 1
        self
494
    }
495

496
    /// Sets the `domain` field in the session cookie being built.
497 1
    pub fn domain<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
498 1
        Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into());
499 1
        self
500
    }
501

502
    /// Sets the `secure` field in the session cookie being built.
503
    ///
504
    /// If the `secure` field is set, a cookie will only be transmitted when the
505
    /// connection is secure - i.e. `https`
506 1
    pub fn secure(mut self, value: bool) -> CookieIdentityPolicy {
507 1
        Rc::get_mut(&mut self.0).unwrap().secure = value;
508 1
        self
509
    }
510

511
    /// Sets the `max-age` field in the session cookie being built with given number of seconds.
512 1
    pub fn max_age(self, seconds: i64) -> CookieIdentityPolicy {
513 1
        self.max_age_time(Duration::seconds(seconds))
514
    }
515

516
    /// Sets the `max-age` field in the session cookie being built with `time::Duration`.
517 1
    pub fn max_age_time(mut self, value: Duration) -> CookieIdentityPolicy {
518 1
        Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
519 1
        self
520
    }
521

522
    /// Sets the `http_only` field in the session cookie being built.
523 1
    pub fn http_only(mut self, http_only: bool) -> Self {
524 1
        Rc::get_mut(&mut self.0).unwrap().http_only = Some(http_only);
525 1
        self
526
    }
527

528
    /// Sets the `same_site` field in the session cookie being built.
529 1
    pub fn same_site(mut self, same_site: SameSite) -> Self {
530 1
        Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site);
531 1
        self
532
    }
533

534
    /// Accepts only users whose cookie has been seen before the given deadline
535
    ///
536
    /// By default visit deadline is disabled.
537 1
    pub fn visit_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
538 1
        Rc::get_mut(&mut self.0).unwrap().visit_deadline = Some(value);
539 1
        self
540
    }
541

542
    /// Accepts only users which has been authenticated before the given deadline
543
    ///
544
    /// By default login deadline is disabled.
545 1
    pub fn login_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
546 1
        Rc::get_mut(&mut self.0).unwrap().login_deadline = Some(value);
547 1
        self
548
    }
549
}
550

551
impl IdentityPolicy for CookieIdentityPolicy {
552
    type Future = Ready<Result<Option<String>, Error>>;
553
    type ResponseFuture = Ready<Result<(), Error>>;
554

555 1
    fn from_request(&self, req: &mut ServiceRequest) -> Self::Future {
556 1
        ok(self.0.load(req).map(
557 1
            |CookieValue {
558 1
                 identity,
559 1
                 login_timestamp,
560 0
                 ..
561 0
             }| {
562 1
                if self.0.requires_oob_data() {
563 1
                    req.extensions_mut()
564 1
                        .insert(CookieIdentityExtension { login_timestamp });
565
                }
566 0
                identity
567
            },
568
        ))
569
    }
570

571 1
    fn to_response<B>(
572
        &self,
573
        id: Option<String>,
574
        changed: bool,
575
        res: &mut ServiceResponse<B>,
576
    ) -> Self::ResponseFuture {
577 1
        let _ = if changed {
578 1
            let login_timestamp = SystemTime::now();
579 1
            self.0.set_cookie(
580 0
                res,
581 1
                id.map(|identity| CookieValue {
582 1
                    identity,
583 1
                    login_timestamp: self.0.login_deadline.map(|_| login_timestamp),
584 1
                    visit_timestamp: self.0.visit_deadline.map(|_| login_timestamp),
585
                }),
586
            )
587 1
        } else if self.0.always_update_cookie() && id.is_some() {
588 1
            let visit_timestamp = SystemTime::now();
589 1
            let login_timestamp = if self.0.requires_oob_data() {
590 1
                let CookieIdentityExtension {
591 1
                    login_timestamp: lt,
592 0
                } = res.request().extensions_mut().remove().unwrap();
593 1
                lt
594
            } else {
595 0
                None
596
            };
597 1
            self.0.set_cookie(
598 0
                res,
599 1
                Some(CookieValue {
600 1
                    identity: id.unwrap(),
601 1
                    login_timestamp,
602 1
                    visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp),
603
                }),
604
            )
605
        } else {
606 1
            Ok(())
607
        };
608 1
        ok(())
609
    }
610
}
611

612
#[cfg(test)]
613
mod tests {
614
    use std::borrow::Borrow;
615

616
    use super::*;
617
    use actix_service::into_service;
618
    use actix_web::http::StatusCode;
619
    use actix_web::test::{self, TestRequest};
620
    use actix_web::{error, web, App, Error, HttpResponse};
621

622
    const COOKIE_KEY_MASTER: [u8; 32] = [0; 32];
623
    const COOKIE_NAME: &str = "actix_auth";
624
    const COOKIE_LOGIN: &str = "test";
625

626 1
    #[actix_rt::test]
627 1
    async fn test_identity() {
628 1
        let srv = test::init_service(
629 1
            App::new()
630 1
                .wrap(IdentityService::new(
631 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
632
                        .domain("www.rust-lang.org")
633
                        .name(COOKIE_NAME)
634
                        .path("/")
635
                        .secure(true),
636
                ))
637 1
                .service(web::resource("/index").to(|id: Identity| {
638 1
                    if id.identity().is_some() {
639 1
                        HttpResponse::Created()
640
                    } else {
641 1
                        HttpResponse::Ok()
642
                    }
643
                }))
644 1
                .service(web::resource("/login").to(|id: Identity| {
645 1
                    id.remember(COOKIE_LOGIN.to_string());
646 1
                    HttpResponse::Ok()
647
                }))
648 1
                .service(web::resource("/logout").to(|id: Identity| {
649 1
                    if id.identity().is_some() {
650 1
                        id.forget();
651 1
                        HttpResponse::Ok()
652
                    } else {
653 0
                        HttpResponse::BadRequest()
654
                    }
655
                })),
656
        )
657
        .await;
658 1
        let resp =
659
            test::call_service(&srv, TestRequest::with_uri("/index").to_request()).await;
660 1
        assert_eq!(resp.status(), StatusCode::OK);
661

662 1
        let resp =
663
            test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
664 1
        assert_eq!(resp.status(), StatusCode::OK);
665 1
        let c = resp.response().cookies().next().unwrap().to_owned();
666

667 1
        let resp = test::call_service(
668
            &srv,
669 1
            TestRequest::with_uri("/index")
670 1
                .cookie(c.clone())
671
                .to_request(),
672
        )
673
        .await;
674 1
        assert_eq!(resp.status(), StatusCode::CREATED);
675

676 1
        let resp = test::call_service(
677
            &srv,
678 1
            TestRequest::with_uri("/logout")
679 1
                .cookie(c.clone())
680
                .to_request(),
681
        )
682
        .await;
683 1
        assert_eq!(resp.status(), StatusCode::OK);
684 1
        assert!(resp.headers().contains_key(header::SET_COOKIE))
685
    }
686

687 1
    #[actix_rt::test]
688 1
    async fn test_identity_max_age_time() {
689 1
        let duration = Duration::days(1);
690 1
        let srv = test::init_service(
691 1
            App::new()
692 1
                .wrap(IdentityService::new(
693 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
694
                        .domain("www.rust-lang.org")
695
                        .name(COOKIE_NAME)
696
                        .path("/")
697 1
                        .max_age_time(duration)
698
                        .secure(true),
699
                ))
700 1
                .service(web::resource("/login").to(|id: Identity| {
701 1
                    id.remember("test".to_string());
702 1
                    HttpResponse::Ok()
703
                })),
704
        )
705
        .await;
706 1
        let resp =
707
            test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
708 1
        assert_eq!(resp.status(), StatusCode::OK);
709 1
        assert!(resp.headers().contains_key(header::SET_COOKIE));
710 1
        let c = resp.response().cookies().next().unwrap().to_owned();
711 1
        assert_eq!(duration, c.max_age().unwrap());
712
    }
713

714 1
    #[actix_rt::test]
715 1
    async fn test_http_only_same_site() {
716 1
        let srv = test::init_service(
717 1
            App::new()
718 1
                .wrap(IdentityService::new(
719 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
720
                        .domain("www.rust-lang.org")
721
                        .name(COOKIE_NAME)
722
                        .path("/")
723
                        .http_only(true)
724 1
                        .same_site(SameSite::None),
725
                ))
726 1
                .service(web::resource("/login").to(|id: Identity| {
727 1
                    id.remember("test".to_string());
728 1
                    HttpResponse::Ok()
729
                })),
730
        )
731
        .await;
732

733 1
        let resp =
734
            test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
735

736 1
        assert_eq!(resp.status(), StatusCode::OK);
737 1
        assert!(resp.headers().contains_key(header::SET_COOKIE));
738

739 1
        let c = resp.response().cookies().next().unwrap().to_owned();
740 1
        assert!(c.http_only().unwrap());
741 1
        assert_eq!(SameSite::None, c.same_site().unwrap());
742
    }
743

744 1
    #[actix_rt::test]
745 1
    async fn test_identity_max_age() {
746 1
        let seconds = 60;
747 1
        let srv = test::init_service(
748 1
            App::new()
749 1
                .wrap(IdentityService::new(
750 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
751
                        .domain("www.rust-lang.org")
752
                        .name(COOKIE_NAME)
753
                        .path("/")
754 1
                        .max_age(seconds)
755
                        .secure(true),
756
                ))
757 1
                .service(web::resource("/login").to(|id: Identity| {
758 1
                    id.remember("test".to_string());
759 1
                    HttpResponse::Ok()
760
                })),
761
        )
762
        .await;
763 1
        let resp =
764
            test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
765 1
        assert_eq!(resp.status(), StatusCode::OK);
766 1
        assert!(resp.headers().contains_key(header::SET_COOKIE));
767 1
        let c = resp.response().cookies().next().unwrap().to_owned();
768 1
        assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap());
769
    }
770

771 1
    async fn create_identity_server<
772
        F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static,
773
    >(
774
        f: F,
775
    ) -> impl actix_service::Service<
776
        actix_http::Request,
777
        Response = ServiceResponse<actix_web::body::Body>,
778
        Error = Error,
779
    > {
780 1
        test::init_service(
781 1
            App::new()
782 1
                .wrap(IdentityService::new(f(CookieIdentityPolicy::new(
783
                    &COOKIE_KEY_MASTER,
784
                )
785
                .secure(false)
786 1
                .name(COOKIE_NAME))))
787 1
                .service(web::resource("/").to(|id: Identity| async move {
788 1
                    let identity = id.identity();
789 1
                    if identity.is_none() {
790 1
                        id.remember(COOKIE_LOGIN.to_string())
791
                    }
792 1
                    web::Json(identity)
793
                })),
794
        )
795
        .await
796
    }
797

798 1
    fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> {
799 1
        let mut jar = CookieJar::new();
800 1
        jar.private(&Key::derive_from(&COOKIE_KEY_MASTER))
801 1
            .add(Cookie::new(COOKIE_NAME, identity));
802 1
        jar.get(COOKIE_NAME).unwrap().clone()
803
    }
804

805 1
    fn login_cookie(
806
        identity: &'static str,
807
        login_timestamp: Option<SystemTime>,
808
        visit_timestamp: Option<SystemTime>,
809
    ) -> Cookie<'static> {
810 1
        let mut jar = CookieJar::new();
811 1
        let key: Vec<u8> = COOKIE_KEY_MASTER
812
            .iter()
813 1
            .chain([1, 0, 0, 0].iter())
814
            .copied()
815
            .collect();
816 1
        jar.private(&Key::derive_from(&key)).add(Cookie::new(
817
            COOKIE_NAME,
818 1
            serde_json::to_string(&CookieValue {
819 1
                identity: identity.to_string(),
820 1
                login_timestamp,
821 1
                visit_timestamp,
822
            })
823
            .unwrap(),
824
        ));
825 1
        jar.get(COOKIE_NAME).unwrap().clone()
826
    }
827

828 1
    async fn assert_logged_in(response: ServiceResponse, identity: Option<&str>) {
829 1
        let bytes = test::read_body(response).await;
830 1
        let resp: Option<String> = serde_json::from_slice(&bytes[..]).unwrap();
831 1
        assert_eq!(resp.as_ref().map(|s| s.borrow()), identity);
832
    }
833

834 1
    fn assert_legacy_login_cookie(response: &mut ServiceResponse, identity: &str) {
835 1
        let mut cookies = CookieJar::new();
836 1
        for cookie in response.headers().get_all(header::SET_COOKIE) {
837 1
            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
838
        }
839 1
        let cookie = cookies
840 1
            .private(&Key::derive_from(&COOKIE_KEY_MASTER))
841
            .get(COOKIE_NAME)
842
            .unwrap();
843 1
        assert_eq!(cookie.value(), identity);
844
    }
845

846
    #[allow(clippy::enum_variant_names)]
847
    enum LoginTimestampCheck {
848
        NoTimestamp,
849
        NewTimestamp,
850
        OldTimestamp(SystemTime),
851
    }
852

853
    #[allow(clippy::enum_variant_names)]
854
    enum VisitTimeStampCheck {
855
        NoTimestamp,
856
        NewTimestamp,
857
    }
858

859 1
    fn assert_login_cookie(
860
        response: &mut ServiceResponse,
861
        identity: &str,
862
        login_timestamp: LoginTimestampCheck,
863
        visit_timestamp: VisitTimeStampCheck,
864
    ) {
865 1
        let mut cookies = CookieJar::new();
866 1
        for cookie in response.headers().get_all(header::SET_COOKIE) {
867 1
            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
868
        }
869 1
        let key: Vec<u8> = COOKIE_KEY_MASTER
870
            .iter()
871 1
            .chain([1, 0, 0, 0].iter())
872
            .copied()
873
            .collect();
874 1
        let cookie = cookies
875 1
            .private(&Key::derive_from(&key))
876
            .get(COOKIE_NAME)
877
            .unwrap();
878 1
        let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap();
879 1
        assert_eq!(cv.identity, identity);
880 1
        let now = SystemTime::now();
881 1
        let t30sec_ago = now - Duration::seconds(30);
882 1
        match login_timestamp {
883 1
            LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None),
884 1
            LoginTimestampCheck::NewTimestamp => assert!(
885 1
                t30sec_ago <= cv.login_timestamp.unwrap()
886 1
                    && cv.login_timestamp.unwrap() <= now
887
            ),
888 1
            LoginTimestampCheck::OldTimestamp(old_timestamp) => {
889 1
                assert_eq!(cv.login_timestamp, Some(old_timestamp))
890
            }
891
        }
892 1
        match visit_timestamp {
893 1
            VisitTimeStampCheck::NoTimestamp => assert_eq!(cv.visit_timestamp, None),
894 1
            VisitTimeStampCheck::NewTimestamp => assert!(
895 1
                t30sec_ago <= cv.visit_timestamp.unwrap()
896 1
                    && cv.visit_timestamp.unwrap() <= now
897
            ),
898
        }
899
    }
900

901 1
    fn assert_no_login_cookie(response: &mut ServiceResponse) {
902 1
        let mut cookies = CookieJar::new();
903 1
        for cookie in response.headers().get_all(header::SET_COOKIE) {
904 0
            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
905
        }
906 1
        assert!(cookies.get(COOKIE_NAME).is_none());
907
    }
908

909 1
    #[actix_rt::test]
910 1
    async fn test_identity_legacy_cookie_is_set() {
911 1
        let srv = create_identity_server(|c| c).await;
912 1
        let mut resp =
913
            test::call_service(&srv, TestRequest::with_uri("/").to_request()).await;
914 1
        assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN);
915 1
        assert_logged_in(resp, None).await;
916
    }
917

918 1
    #[actix_rt::test]
919 1
    async fn test_identity_legacy_cookie_works() {
920 1
        let srv = create_identity_server(|c| c).await;
921 1
        let cookie = legacy_login_cookie(COOKIE_LOGIN);
922 1
        let mut resp = test::call_service(
923
            &srv,
924 1
            TestRequest::with_uri("/")
925 1
                .cookie(cookie.clone())
926
                .to_request(),
927
        )
928
        .await;
929 1
        assert_no_login_cookie(&mut resp);
930 1
        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
931
    }
932

933 1
    #[actix_rt::test]
934 1
    async fn test_identity_legacy_cookie_rejected_if_visit_timestamp_needed() {
935 1
        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
936 1
        let cookie = legacy_login_cookie(COOKIE_LOGIN);
937 1
        let mut resp = test::call_service(
938
            &srv,
939 1
            TestRequest::with_uri("/")
940 1
                .cookie(cookie.clone())
941
                .to_request(),
942
        )
943
        .await;
944
        assert_login_cookie(
945 1
            &mut resp,
946
            COOKIE_LOGIN,
947 1
            LoginTimestampCheck::NoTimestamp,
948 1
            VisitTimeStampCheck::NewTimestamp,
949
        );
950 1
        assert_logged_in(resp, None).await;
951
    }
952

953 1
    #[actix_rt::test]
954 1
    async fn test_identity_legacy_cookie_rejected_if_login_timestamp_needed() {
955 1
        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
956 1
        let cookie = legacy_login_cookie(COOKIE_LOGIN);
957 1
        let mut resp = test::call_service(
958
            &srv,
959 1
            TestRequest::with_uri("/")
960 1
                .cookie(cookie.clone())
961
                .to_request(),
962
        )
963
        .await;
964
        assert_login_cookie(
965 1
            &mut resp,
966
            COOKIE_LOGIN,
967 1
            LoginTimestampCheck::NewTimestamp,
968 1
            VisitTimeStampCheck::NoTimestamp,
969
        );
970 1
        assert_logged_in(resp, None).await;
971
    }
972

973 1
    #[actix_rt::test]
974 1
    async fn test_identity_cookie_rejected_if_login_timestamp_needed() {
975 1
        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
976 1
        let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now()));
977 1
        let mut resp = test::call_service(
978
            &srv,
979 1
            TestRequest::with_uri("/")
980 1
                .cookie(cookie.clone())
981
                .to_request(),
982
        )
983
        .await;
984
        assert_login_cookie(
985 1
            &mut resp,
986
            COOKIE_LOGIN,
987 1
            LoginTimestampCheck::NewTimestamp,
988 1
            VisitTimeStampCheck::NoTimestamp,
989
        );
990 1
        assert_logged_in(resp, None).await;
991
    }
992

993 1
    #[actix_rt::test]
994 1
    async fn test_identity_cookie_rejected_if_visit_timestamp_needed() {
995 1
        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
996 1
        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
997 1
        let mut resp = test::call_service(
998
            &srv,
999 1
            TestRequest::with_uri("/")
1000 1
                .cookie(cookie.clone())
1001
                .to_request(),
1002
        )
1003
        .await;
1004
        assert_login_cookie(
1005 1
            &mut resp,
1006
            COOKIE_LOGIN,
1007 1
            LoginTimestampCheck::NoTimestamp,
1008 1
            VisitTimeStampCheck::NewTimestamp,
1009
        );
1010 1
        assert_logged_in(resp, None).await;
1011
    }
1012

1013 1
    #[actix_rt::test]
1014 1
    async fn test_identity_cookie_rejected_if_login_timestamp_too_old() {
1015 1
        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
1016
        let cookie = login_cookie(
1017
            COOKIE_LOGIN,
1018 1
            Some(SystemTime::now() - Duration::days(180)),
1019 1
            None,
1020
        );
1021 1
        let mut resp = test::call_service(
1022
            &srv,
1023 1
            TestRequest::with_uri("/")
1024 1
                .cookie(cookie.clone())
1025
                .to_request(),
1026
        )
1027
        .await;
1028
        assert_login_cookie(
1029 1
            &mut resp,
1030
            COOKIE_LOGIN,
1031 1
            LoginTimestampCheck::NewTimestamp,
1032 1
            VisitTimeStampCheck::NoTimestamp,
1033
        );
1034 1
        assert_logged_in(resp, None).await;
1035
    }
1036

1037 1
    #[actix_rt::test]
1038 1
    async fn test_identity_cookie_rejected_if_visit_timestamp_too_old() {
1039 1
        let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
1040
        let cookie = login_cookie(
1041
            COOKIE_LOGIN,
1042 1
            None,
1043 1
            Some(SystemTime::now() - Duration::days(180)),
1044
        );
1045 1
        let mut resp = test::call_service(
1046
            &srv,
1047 1
            TestRequest::with_uri("/")
1048 1
                .cookie(cookie.clone())
1049
                .to_request(),
1050
        )
1051
        .await;
1052
        assert_login_cookie(
1053 1
            &mut resp,
1054
            COOKIE_LOGIN,
1055 1
            LoginTimestampCheck::NoTimestamp,
1056 1
            VisitTimeStampCheck::NewTimestamp,
1057
        );
1058 1
        assert_logged_in(resp, None).await;
1059
    }
1060

1061 1
    #[actix_rt::test]
1062 1
    async fn test_identity_cookie_not_updated_on_login_deadline() {
1063 1
        let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
1064 1
        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
1065 1
        let mut resp = test::call_service(
1066
            &srv,
1067 1
            TestRequest::with_uri("/")
1068 1
                .cookie(cookie.clone())
1069
                .to_request(),
1070
        )
1071
        .await;
1072 1
        assert_no_login_cookie(&mut resp);
1073 1
        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
1074
    }
1075

1076
    // https://github.com/actix/actix-web/issues/1263
1077 1
    #[actix_rt::test]
1078 1
    async fn test_identity_cookie_updated_on_visit_deadline() {
1079 1
        let srv = create_identity_server(|c| {
1080 1
            c.visit_deadline(Duration::days(90))
1081 1
                .login_deadline(Duration::days(90))
1082
        })
1083
        .await;
1084 1
        let timestamp = SystemTime::now() - Duration::days(1);
1085 1
        let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
1086 1
        let mut resp = test::call_service(
1087
            &srv,
1088 1
            TestRequest::with_uri("/")
1089 1
                .cookie(cookie.clone())
1090
                .to_request(),
1091
        )
1092
        .await;
1093
        assert_login_cookie(
1094 1
            &mut resp,
1095
            COOKIE_LOGIN,
1096 1
            LoginTimestampCheck::OldTimestamp(timestamp),
1097 1
            VisitTimeStampCheck::NewTimestamp,
1098
        );
1099 1
        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
1100
    }
1101

1102 1
    #[actix_rt::test]
1103 1
    async fn test_borrowed_mut_error() {
1104
        use futures_util::future::{lazy, ok, Ready};
1105

1106
        struct Ident;
1107
        impl IdentityPolicy for Ident {
1108
            type Future = Ready<Result<Option<String>, Error>>;
1109
            type ResponseFuture = Ready<Result<(), Error>>;
1110

1111 1
            fn from_request(&self, _: &mut ServiceRequest) -> Self::Future {
1112 1
                ok(Some("test".to_string()))
1113
            }
1114

1115 0
            fn to_response<B>(
1116
                &self,
1117
                _: Option<String>,
1118
                _: bool,
1119
                _: &mut ServiceResponse<B>,
1120
            ) -> Self::ResponseFuture {
1121 0
                ok(())
1122
            }
1123
        }
1124

1125
        let srv = IdentityServiceMiddleware {
1126 1
            backend: Rc::new(Ident),
1127 1
            service: Rc::new(into_service(|_: ServiceRequest| async move {
1128
                actix_rt::time::sleep(std::time::Duration::from_secs(100)).await;
1129
                Err::<ServiceResponse, _>(error::ErrorBadRequest("error"))
1130
            })),
1131
        };
1132

1133 1
        let srv2 = srv.clone();
1134 1
        let req = TestRequest::default().to_srv_request();
1135 1
        actix_rt::spawn(async move {
1136 1
            let _ = srv2.call(req).await;
1137
        });
1138 1
        actix_rt::time::sleep(std::time::Duration::from_millis(50)).await;
1139

1140 1
        let _ = lazy(|cx| srv.poll_ready(cx)).await;
1141
    }
1142
}

Read our documentation on viewing source code .

Loading