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
//! ```rust
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::cell::RefCell;
53
use std::future::Future;
54
use std::rc::Rc;
55
use std::task::{Context, Poll};
56
use std::time::SystemTime;
57

58
use actix_service::{Service, Transform};
59
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
60
use serde::{Deserialize, Serialize};
61
use time::Duration;
62

63
use actix_web::cookie::{Cookie, CookieJar, Key, SameSite};
64
use actix_web::dev::{Extensions, Payload, ServiceRequest, ServiceResponse};
65
use actix_web::error::{Error, Result};
66
use actix_web::http::header::{self, HeaderValue};
67
use actix_web::{FromRequest, HttpMessage, HttpRequest};
68

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

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

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

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

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

131
struct IdentityItem {
132
    id: Option<String>,
133
    changed: bool,
134
}
135

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

144
impl<T> RequestIdentity for T
145
where
146
    T: HttpMessage,
147
{
148 0
    fn get_identity(&self) -> Option<String> {
149 0
        Identity::get_identity(&self.extensions())
150
    }
151
}
152

153
/// Extractor implementation for Identity type.
154
///
155
/// ```rust
156
/// # use actix_web::*;
157
/// use actix_identity::Identity;
158
///
159
/// fn index(id: Identity) -> String {
160
///     // access request identity
161
///     if let Some(id) = id.identity() {
162
///         format!("Welcome! {}", id)
163
///     } else {
164
///         "Welcome Anonymous!".to_owned()
165
///     }
166
/// }
167
/// # fn main() {}
168
/// ```
169
impl FromRequest for Identity {
170
    type Config = ();
171
    type Error = Error;
172
    type Future = Ready<Result<Identity, Error>>;
173

174
    #[inline]
175 1
    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
176 1
        ok(Identity(req.clone()))
177
    }
178
}
179

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

185
    /// The return type of the middleware
186
    type ResponseFuture: Future<Output = Result<(), Error>>;
187

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

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

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

217
impl<T> IdentityService<T> {
218
    /// Create new identity service with specified backend.
219 1
    pub fn new(backend: T) -> Self {
220
        IdentityService {
221 1
            backend: Rc::new(backend),
222
        }
223
    }
224
}
225

226
impl<S, T, B> Transform<S> for IdentityService<T>
227
where
228
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>
229
        + 'static,
230
    S::Future: 'static,
231
    T: IdentityPolicy,
232
    B: 'static,
233
{
234
    type Request = ServiceRequest;
235
    type Response = ServiceResponse<B>;
236
    type Error = Error;
237
    type InitError = ();
238
    type Transform = IdentityServiceMiddleware<S, T>;
239
    type Future = Ready<Result<Self::Transform, Self::InitError>>;
240

241 1
    fn new_transform(&self, service: S) -> Self::Future {
242 1
        ok(IdentityServiceMiddleware {
243 1
            backend: self.backend.clone(),
244 1
            service: Rc::new(RefCell::new(service)),
245
        })
246
    }
247
}
248

249
#[doc(hidden)]
250
pub struct IdentityServiceMiddleware<S, T> {
251
    backend: Rc<T>,
252
    service: Rc<RefCell<S>>,
253
}
254

255
impl<S, T> Clone for IdentityServiceMiddleware<S, T> {
256 1
    fn clone(&self) -> Self {
257
        Self {
258 1
            backend: self.backend.clone(),
259 1
            service: self.service.clone(),
260
        }
261
    }
262
}
263

264
impl<S, T, B> Service for IdentityServiceMiddleware<S, T>
265
where
266
    B: 'static,
267
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>
268
        + 'static,
269
    S::Future: 'static,
270
    T: IdentityPolicy,
271
{
272
    type Request = ServiceRequest;
273
    type Response = ServiceResponse<B>;
274
    type Error = Error;
275
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
276

277 1
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
278 1
        self.service.borrow_mut().poll_ready(cx)
279
    }
280

281 1
    fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
282 1
        let srv = self.service.clone();
283 1
        let backend = self.backend.clone();
284 1
        let fut = self.backend.from_request(&mut req);
285

286 1
        async move {
287 1
            match fut.await {
288 1
                Ok(id) => {
289 1
                    req.extensions_mut()
290 1
                        .insert(IdentityItem { id, changed: false });
291

292
                    // https://github.com/actix/actix-web/issues/1263
293 1
                    let fut = srv.borrow_mut().call(req);
294 1
                    let mut res = fut.await?;
295 1
                    let id = res.request().extensions_mut().remove::<IdentityItem>();
296

297 1
                    if let Some(id) = id {
298 1
                        match backend.to_response(id.id, id.changed, &mut res).await {
299 1
                            Ok(_) => Ok(res),
300 0
                            Err(e) => Ok(res.error_response(e)),
301
                        }
302
                    } else {
303 0
                        Ok(res)
304
                    }
305
                }
306 0
                Err(err) => Ok(req.error_response(err)),
307
            }
308
        }
309
        .boxed_local()
310
    }
311
}
312

313
struct CookieIdentityInner {
314
    key: Key,
315
    key_v2: Key,
316
    name: String,
317
    path: String,
318
    domain: Option<String>,
319
    secure: bool,
320
    max_age: Option<Duration>,
321
    http_only: Option<bool>,
322
    same_site: Option<SameSite>,
323
    visit_deadline: Option<Duration>,
324
    login_deadline: Option<Duration>,
325
}
326

327
#[derive(Deserialize, Serialize, Debug)]
328
struct CookieValue {
329
    identity: String,
330

331
    #[serde(skip_serializing_if = "Option::is_none")]
332
    login_timestamp: Option<SystemTime>,
333

334
    #[serde(skip_serializing_if = "Option::is_none")]
335
    visit_timestamp: Option<SystemTime>,
336
}
337

338
#[derive(Debug)]
339
struct CookieIdentityExtension {
340
    login_timestamp: Option<SystemTime>,
341
}
342

343
impl CookieIdentityInner {
344 1
    fn new(key: &[u8]) -> CookieIdentityInner {
345 1
        let key_v2: Vec<u8> = key.iter().chain([1, 0, 0, 0].iter()).cloned().collect();
346
        CookieIdentityInner {
347 1
            key: Key::derive_from(key),
348 1
            key_v2: Key::derive_from(&key_v2),
349 1
            name: "actix-identity".to_owned(),
350 1
            path: "/".to_owned(),
351
            domain: None,
352
            secure: true,
353
            max_age: None,
354
            http_only: None,
355
            same_site: None,
356
            visit_deadline: None,
357
            login_deadline: None,
358
        }
359
    }
360

361 1
    fn set_cookie<B>(
362
        &self,
363
        resp: &mut ServiceResponse<B>,
364
        value: Option<CookieValue>,
365
    ) -> Result<()> {
366 1
        let add_cookie = value.is_some();
367 1
        let val = value.map(|val| {
368 1
            if !self.legacy_supported() {
369 1
                serde_json::to_string(&val)
370
            } else {
371 1
                Ok(val.identity)
372
            }
373
        });
374 1
        let mut cookie =
375 0
            Cookie::new(self.name.clone(), val.unwrap_or_else(|| Ok(String::new()))?);
376 1
        cookie.set_path(self.path.clone());
377 1
        cookie.set_secure(self.secure);
378 1
        cookie.set_http_only(true);
379

380 1
        if let Some(ref domain) = self.domain {
381 1
            cookie.set_domain(domain.clone());
382
        }
383

384 1
        if let Some(max_age) = self.max_age {
385 1
            cookie.set_max_age(max_age);
386
        }
387

388 1
        if let Some(http_only) = self.http_only {
389 1
            cookie.set_http_only(http_only);
390
        }
391

392 1
        if let Some(same_site) = self.same_site {
393 1
            cookie.set_same_site(same_site);
394
        }
395

396 1
        let mut jar = CookieJar::new();
397 1
        let key = if self.legacy_supported() {
398 1
            &self.key
399
        } else {
400 1
            &self.key_v2
401
        };
402 1
        if add_cookie {
403 1
            jar.private(&key).add(cookie);
404
        } else {
405 1
            jar.add_original(cookie.clone());
406 1
            jar.private(&key).remove(cookie);
407
        }
408 1
        for cookie in jar.delta() {
409 1
            let val = HeaderValue::from_str(&cookie.to_string())?;
410 1
            resp.headers_mut().append(header::SET_COOKIE, val);
411
        }
412 1
        Ok(())
413
    }
414

415 1
    fn load(&self, req: &ServiceRequest) -> Option<CookieValue> {
416 1
        let cookie = req.cookie(&self.name)?;
417 1
        let mut jar = CookieJar::new();
418 1
        jar.add_original(cookie.clone());
419 1
        let res = if self.legacy_supported() {
420 1
            jar.private(&self.key).get(&self.name).map(|n| CookieValue {
421 1
                identity: n.value().to_string(),
422 1
                login_timestamp: None,
423 1
                visit_timestamp: None,
424
            })
425
        } else {
426 1
            None
427
        };
428 1
        res.or_else(|| {
429 1
            jar.private(&self.key_v2)
430 1
                .get(&self.name)
431 1
                .and_then(|c| self.parse(c))
432
        })
433
    }
434

435 1
    fn parse(&self, cookie: Cookie<'_>) -> Option<CookieValue> {
436 1
        let value: CookieValue = serde_json::from_str(cookie.value()).ok()?;
437 1
        let now = SystemTime::now();
438 1
        if let Some(visit_deadline) = self.visit_deadline {
439 1
            if now.duration_since(value.visit_timestamp?).ok()? > visit_deadline {
440 1
                return None;
441
            }
442
        }
443 1
        if let Some(login_deadline) = self.login_deadline {
444 1
            if now.duration_since(value.login_timestamp?).ok()? > login_deadline {
445 1
                return None;
446
            }
447
        }
448 1
        Some(value)
449
    }
450

451 1
    fn legacy_supported(&self) -> bool {
452 1
        self.visit_deadline.is_none() && self.login_deadline.is_none()
453
    }
454

455 1
    fn always_update_cookie(&self) -> bool {
456 1
        self.visit_deadline.is_some()
457
    }
458

459 1
    fn requires_oob_data(&self) -> bool {
460 1
        self.login_deadline.is_some()
461
    }
462
}
463

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

488
impl CookieIdentityPolicy {
489
    /// Construct new `CookieIdentityPolicy` instance.
490
    ///
491
    /// Panics if key length is less than 32 bytes.
492 1
    pub fn new(key: &[u8]) -> CookieIdentityPolicy {
493 1
        CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key)))
494
    }
495

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

502
    /// Sets the `name` field in the session cookie being built.
503 1
    pub fn name<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
504 1
        Rc::get_mut(&mut self.0).unwrap().name = value.into();
505 1
        self
506
    }
507

508
    /// Sets the `domain` field in the session cookie being built.
509 1
    pub fn domain<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
510 1
        Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into());
511 1
        self
512
    }
513

514
    /// Sets the `secure` field in the session cookie being built.
515
    ///
516
    /// If the `secure` field is set, a cookie will only be transmitted when the
517
    /// connection is secure - i.e. `https`
518 1
    pub fn secure(mut self, value: bool) -> CookieIdentityPolicy {
519 1
        Rc::get_mut(&mut self.0).unwrap().secure = value;
520 1
        self
521
    }
522

523
    /// Sets the `max-age` field in the session cookie being built with given number of seconds.
524 1
    pub fn max_age(self, seconds: i64) -> CookieIdentityPolicy {
525 1
        self.max_age_time(Duration::seconds(seconds))
526
    }
527

528
    /// Sets the `max-age` field in the session cookie being built with `time::Duration`.
529 1
    pub fn max_age_time(mut self, value: Duration) -> CookieIdentityPolicy {
530 1
        Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
531 1
        self
532
    }
533

534
    /// Sets the `http_only` field in the session cookie being built.
535 1
    pub fn http_only(mut self, http_only: bool) -> Self {
536 1
        Rc::get_mut(&mut self.0).unwrap().http_only = Some(http_only);
537 1
        self
538
    }
539

540
    /// Sets the `same_site` field in the session cookie being built.
541 1
    pub fn same_site(mut self, same_site: SameSite) -> Self {
542 1
        Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site);
543 1
        self
544
    }
545

546
    /// Accepts only users whose cookie has been seen before the given deadline
547
    ///
548
    /// By default visit deadline is disabled.
549 1
    pub fn visit_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
550 1
        Rc::get_mut(&mut self.0).unwrap().visit_deadline = Some(value);
551 1
        self
552
    }
553

554
    /// Accepts only users which has been authenticated before the given deadline
555
    ///
556
    /// By default login deadline is disabled.
557 1
    pub fn login_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
558 1
        Rc::get_mut(&mut self.0).unwrap().login_deadline = Some(value);
559 1
        self
560
    }
561
}
562

563
impl IdentityPolicy for CookieIdentityPolicy {
564
    type Future = Ready<Result<Option<String>, Error>>;
565
    type ResponseFuture = Ready<Result<(), Error>>;
566

567 1
    fn from_request(&self, req: &mut ServiceRequest) -> Self::Future {
568 1
        ok(self.0.load(req).map(
569 1
            |CookieValue {
570 1
                 identity,
571 1
                 login_timestamp,
572 0
                 ..
573 0
             }| {
574 1
                if self.0.requires_oob_data() {
575 1
                    req.extensions_mut()
576 1
                        .insert(CookieIdentityExtension { login_timestamp });
577
                }
578 0
                identity
579
            },
580
        ))
581
    }
582

583 1
    fn to_response<B>(
584
        &self,
585
        id: Option<String>,
586
        changed: bool,
587
        res: &mut ServiceResponse<B>,
588
    ) -> Self::ResponseFuture {
589 1
        let _ = if changed {
590 1
            let login_timestamp = SystemTime::now();
591 1
            self.0.set_cookie(
592 0
                res,
593 1
                id.map(|identity| CookieValue {
594 1
                    identity,
595 1
                    login_timestamp: self.0.login_deadline.map(|_| login_timestamp),
596 1
                    visit_timestamp: self.0.visit_deadline.map(|_| login_timestamp),
597
                }),
598
            )
599 1
        } else if self.0.always_update_cookie() && id.is_some() {
600 1
            let visit_timestamp = SystemTime::now();
601 1
            let login_timestamp = if self.0.requires_oob_data() {
602 1
                let CookieIdentityExtension {
603 1
                    login_timestamp: lt,
604 0
                } = res.request().extensions_mut().remove().unwrap();
605 1
                lt
606
            } else {
607 0
                None
608
            };
609 1
            self.0.set_cookie(
610 0
                res,
611 1
                Some(CookieValue {
612 1
                    identity: id.unwrap(),
613 1
                    login_timestamp,
614 1
                    visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp),
615
                }),
616
            )
617
        } else {
618 1
            Ok(())
619
        };
620 1
        ok(())
621
    }
622
}
623

624
#[cfg(test)]
625
mod tests {
626
    use std::borrow::Borrow;
627

628
    use super::*;
629
    use actix_service::into_service;
630
    use actix_web::http::StatusCode;
631
    use actix_web::test::{self, TestRequest};
632
    use actix_web::{error, web, App, Error, HttpResponse};
633

634
    const COOKIE_KEY_MASTER: [u8; 32] = [0; 32];
635
    const COOKIE_NAME: &str = "actix_auth";
636
    const COOKIE_LOGIN: &str = "test";
637

638 1
    #[actix_rt::test]
639 1
    async fn test_identity() {
640 1
        let mut srv = test::init_service(
641 1
            App::new()
642 1
                .wrap(IdentityService::new(
643 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
644
                        .domain("www.rust-lang.org")
645
                        .name(COOKIE_NAME)
646
                        .path("/")
647
                        .secure(true),
648
                ))
649 1
                .service(web::resource("/index").to(|id: Identity| {
650 1
                    if id.identity().is_some() {
651 1
                        HttpResponse::Created()
652
                    } else {
653 1
                        HttpResponse::Ok()
654
                    }
655
                }))
656 1
                .service(web::resource("/login").to(|id: Identity| {
657 1
                    id.remember(COOKIE_LOGIN.to_string());
658 1
                    HttpResponse::Ok()
659
                }))
660 1
                .service(web::resource("/logout").to(|id: Identity| {
661 1
                    if id.identity().is_some() {
662 1
                        id.forget();
663 1
                        HttpResponse::Ok()
664
                    } else {
665 0
                        HttpResponse::BadRequest()
666
                    }
667
                })),
668
        )
669
        .await;
670 1
        let resp =
671
            test::call_service(&mut srv, TestRequest::with_uri("/index").to_request())
672
                .await;
673 1
        assert_eq!(resp.status(), StatusCode::OK);
674

675 1
        let resp =
676
            test::call_service(&mut srv, TestRequest::with_uri("/login").to_request())
677
                .await;
678 1
        assert_eq!(resp.status(), StatusCode::OK);
679 1
        let c = resp.response().cookies().next().unwrap().to_owned();
680

681 1
        let resp = test::call_service(
682
            &mut srv,
683 1
            TestRequest::with_uri("/index")
684 1
                .cookie(c.clone())
685
                .to_request(),
686
        )
687
        .await;
688 1
        assert_eq!(resp.status(), StatusCode::CREATED);
689

690 1
        let resp = test::call_service(
691
            &mut srv,
692 1
            TestRequest::with_uri("/logout")
693 1
                .cookie(c.clone())
694
                .to_request(),
695
        )
696
        .await;
697 1
        assert_eq!(resp.status(), StatusCode::OK);
698 1
        assert!(resp.headers().contains_key(header::SET_COOKIE))
699
    }
700

701 1
    #[actix_rt::test]
702 1
    async fn test_identity_max_age_time() {
703 1
        let duration = Duration::days(1);
704 1
        let mut srv = test::init_service(
705 1
            App::new()
706 1
                .wrap(IdentityService::new(
707 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
708
                        .domain("www.rust-lang.org")
709
                        .name(COOKIE_NAME)
710
                        .path("/")
711 1
                        .max_age_time(duration)
712
                        .secure(true),
713
                ))
714 1
                .service(web::resource("/login").to(|id: Identity| {
715 1
                    id.remember("test".to_string());
716 1
                    HttpResponse::Ok()
717
                })),
718
        )
719
        .await;
720 1
        let resp =
721
            test::call_service(&mut srv, TestRequest::with_uri("/login").to_request())
722
                .await;
723 1
        assert_eq!(resp.status(), StatusCode::OK);
724 1
        assert!(resp.headers().contains_key(header::SET_COOKIE));
725 1
        let c = resp.response().cookies().next().unwrap().to_owned();
726 1
        assert_eq!(duration, c.max_age().unwrap());
727
    }
728

729 1
    #[actix_rt::test]
730 1
    async fn test_http_only_same_site() {
731 1
        let mut srv = test::init_service(
732 1
            App::new()
733 1
                .wrap(IdentityService::new(
734 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
735
                        .domain("www.rust-lang.org")
736
                        .name(COOKIE_NAME)
737
                        .path("/")
738
                        .http_only(true)
739 1
                        .same_site(SameSite::None),
740
                ))
741 1
                .service(web::resource("/login").to(|id: Identity| {
742 1
                    id.remember("test".to_string());
743 1
                    HttpResponse::Ok()
744
                })),
745
        )
746
        .await;
747

748 1
        let resp =
749
            test::call_service(&mut srv, TestRequest::with_uri("/login").to_request())
750
                .await;
751

752 1
        assert_eq!(resp.status(), StatusCode::OK);
753 1
        assert!(resp.headers().contains_key(header::SET_COOKIE));
754

755 1
        let c = resp.response().cookies().next().unwrap().to_owned();
756 1
        assert!(c.http_only().unwrap());
757 1
        assert_eq!(SameSite::None, c.same_site().unwrap());
758
    }
759

760 1
    #[actix_rt::test]
761 1
    async fn test_identity_max_age() {
762 1
        let seconds = 60;
763 1
        let mut srv = test::init_service(
764 1
            App::new()
765 1
                .wrap(IdentityService::new(
766 1
                    CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
767
                        .domain("www.rust-lang.org")
768
                        .name(COOKIE_NAME)
769
                        .path("/")
770 1
                        .max_age(seconds)
771
                        .secure(true),
772
                ))
773 1
                .service(web::resource("/login").to(|id: Identity| {
774 1
                    id.remember("test".to_string());
775 1
                    HttpResponse::Ok()
776
                })),
777
        )
778
        .await;
779 1
        let resp =
780
            test::call_service(&mut srv, TestRequest::with_uri("/login").to_request())
781
                .await;
782 1
        assert_eq!(resp.status(), StatusCode::OK);
783 1
        assert!(resp.headers().contains_key(header::SET_COOKIE));
784 1
        let c = resp.response().cookies().next().unwrap().to_owned();
785 1
        assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap());
786
    }
787

788 1
    async fn create_identity_server<
789
        F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static,
790
    >(
791
        f: F,
792
    ) -> impl actix_service::Service<
793
        Request = actix_http::Request,
794
        Response = ServiceResponse<actix_web::body::Body>,
795
        Error = Error,
796
    > {
797 1
        test::init_service(
798 1
            App::new()
799 1
                .wrap(IdentityService::new(f(CookieIdentityPolicy::new(
800
                    &COOKIE_KEY_MASTER,
801
                )
802
                .secure(false)
803 1
                .name(COOKIE_NAME))))
804 1
                .service(web::resource("/").to(|id: Identity| async move {
805 1
                    let identity = id.identity();
806 1
                    if identity.is_none() {
807 1
                        id.remember(COOKIE_LOGIN.to_string())
808
                    }
809 1
                    web::Json(identity)
810
                })),
811
        )
812
        .await
813
    }
814

815 1
    fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> {
816 1
        let mut jar = CookieJar::new();
817 1
        jar.private(&Key::derive_from(&COOKIE_KEY_MASTER))
818 1
            .add(Cookie::new(COOKIE_NAME, identity));
819 1
        jar.get(COOKIE_NAME).unwrap().clone()
820
    }
821

822 1
    fn login_cookie(
823
        identity: &'static str,
824
        login_timestamp: Option<SystemTime>,
825
        visit_timestamp: Option<SystemTime>,
826
    ) -> Cookie<'static> {
827 1
        let mut jar = CookieJar::new();
828 1
        let key: Vec<u8> = COOKIE_KEY_MASTER
829
            .iter()
830 1
            .chain([1, 0, 0, 0].iter())
831
            .copied()
832
            .collect();
833 1
        jar.private(&Key::derive_from(&key)).add(Cookie::new(
834
            COOKIE_NAME,
835 1
            serde_json::to_string(&CookieValue {
836 1
                identity: identity.to_string(),
837 1
                login_timestamp,
838 1
                visit_timestamp,
839
            })
840
            .unwrap(),
841
        ));
842 1
        jar.get(COOKIE_NAME).unwrap().clone()
843
    }
844

845 1
    async fn assert_logged_in(response: ServiceResponse, identity: Option<&str>) {
846 1
        let bytes = test::read_body(response).await;
847 1
        let resp: Option<String> = serde_json::from_slice(&bytes[..]).unwrap();
848 1
        assert_eq!(resp.as_ref().map(|s| s.borrow()), identity);
849
    }
850

851 1
    fn assert_legacy_login_cookie(response: &mut ServiceResponse, identity: &str) {
852 1
        let mut cookies = CookieJar::new();
853 1
        for cookie in response.headers().get_all(header::SET_COOKIE) {
854 1
            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
855
        }
856 1
        let cookie = cookies
857 1
            .private(&Key::derive_from(&COOKIE_KEY_MASTER))
858
            .get(COOKIE_NAME)
859
            .unwrap();
860 1
        assert_eq!(cookie.value(), identity);
861
    }
862

863
    #[allow(clippy::enum_variant_names)]
864
    enum LoginTimestampCheck {
865
        NoTimestamp,
866
        NewTimestamp,
867
        OldTimestamp(SystemTime),
868
    }
869

870
    #[allow(clippy::enum_variant_names)]
871
    enum VisitTimeStampCheck {
872
        NoTimestamp,
873
        NewTimestamp,
874
    }
875

876 1
    fn assert_login_cookie(
877
        response: &mut ServiceResponse,
878
        identity: &str,
879
        login_timestamp: LoginTimestampCheck,
880
        visit_timestamp: VisitTimeStampCheck,
881
    ) {
882 1
        let mut cookies = CookieJar::new();
883 1
        for cookie in response.headers().get_all(header::SET_COOKIE) {
884 1
            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
885
        }
886 1
        let key: Vec<u8> = COOKIE_KEY_MASTER
887
            .iter()
888 1
            .chain([1, 0, 0, 0].iter())
889
            .copied()
890
            .collect();
891 1
        let cookie = cookies
892 1
            .private(&Key::derive_from(&key))
893
            .get(COOKIE_NAME)
894
            .unwrap();
895 1
        let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap();
896 1
        assert_eq!(cv.identity, identity);
897 1
        let now = SystemTime::now();
898 1
        let t30sec_ago = now - Duration::seconds(30);
899 1
        match login_timestamp {
900 1
            LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None),
901 1
            LoginTimestampCheck::NewTimestamp => assert!(
902 1
                t30sec_ago <= cv.login_timestamp.unwrap()
903 1
                    && cv.login_timestamp.unwrap() <= now
904
            ),
905 1
            LoginTimestampCheck::OldTimestamp(old_timestamp) => {
906 1
                assert_eq!(cv.login_timestamp, Some(old_timestamp))
907
            }
908
        }
909 1
        match visit_timestamp {
910 1
            VisitTimeStampCheck::NoTimestamp => assert_eq!(cv.visit_timestamp, None),
911 1
            VisitTimeStampCheck::NewTimestamp => assert!(
912 1
                t30sec_ago <= cv.visit_timestamp.unwrap()
913 1
                    && cv.visit_timestamp.unwrap() <= now
914
            ),
915
        }
916
    }
917

918 1
    fn assert_no_login_cookie(response: &mut ServiceResponse) {
919 1
        let mut cookies = CookieJar::new();
920 1
        for cookie in response.headers().get_all(header::SET_COOKIE) {
921 0
            cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
922
        }
923 1
        assert!(cookies.get(COOKIE_NAME).is_none());
924
    }
925

926 1
    #[actix_rt::test]
927 1
    async fn test_identity_legacy_cookie_is_set() {
928 1
        let mut srv = create_identity_server(|c| c).await;
929 1
        let mut resp =
930
            test::call_service(&mut srv, TestRequest::with_uri("/").to_request()).await;
931 1
        assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN);
932 1
        assert_logged_in(resp, None).await;
933
    }
934

935 1
    #[actix_rt::test]
936 1
    async fn test_identity_legacy_cookie_works() {
937 1
        let mut srv = create_identity_server(|c| c).await;
938 1
        let cookie = legacy_login_cookie(COOKIE_LOGIN);
939 1
        let mut resp = test::call_service(
940
            &mut srv,
941 1
            TestRequest::with_uri("/")
942 1
                .cookie(cookie.clone())
943
                .to_request(),
944
        )
945
        .await;
946 1
        assert_no_login_cookie(&mut resp);
947 1
        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
948
    }
949

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

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

992 1
    #[actix_rt::test]
993 1
    async fn test_identity_cookie_rejected_if_login_timestamp_needed() {
994 1
        let mut srv =
995
            create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
996 1
        let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now()));
997 1
        let mut resp = test::call_service(
998
            &mut 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::NewTimestamp,
1008 1
            VisitTimeStampCheck::NoTimestamp,
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_visit_timestamp_needed() {
1015 1
        let mut srv =
1016
            create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
1017 1
        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
1018 1
        let mut resp = test::call_service(
1019
            &mut srv,
1020 1
            TestRequest::with_uri("/")
1021 1
                .cookie(cookie.clone())
1022
                .to_request(),
1023
        )
1024
        .await;
1025
        assert_login_cookie(
1026 1
            &mut resp,
1027
            COOKIE_LOGIN,
1028 1
            LoginTimestampCheck::NoTimestamp,
1029 1
            VisitTimeStampCheck::NewTimestamp,
1030
        );
1031 1
        assert_logged_in(resp, None).await;
1032
    }
1033

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

1059 1
    #[actix_rt::test]
1060 1
    async fn test_identity_cookie_rejected_if_visit_timestamp_too_old() {
1061 1
        let mut srv =
1062
            create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
1063
        let cookie = login_cookie(
1064
            COOKIE_LOGIN,
1065 1
            None,
1066 1
            Some(SystemTime::now() - Duration::days(180)),
1067
        );
1068 1
        let mut resp = test::call_service(
1069
            &mut srv,
1070 1
            TestRequest::with_uri("/")
1071 1
                .cookie(cookie.clone())
1072
                .to_request(),
1073
        )
1074
        .await;
1075
        assert_login_cookie(
1076 1
            &mut resp,
1077
            COOKIE_LOGIN,
1078 1
            LoginTimestampCheck::NoTimestamp,
1079 1
            VisitTimeStampCheck::NewTimestamp,
1080
        );
1081 1
        assert_logged_in(resp, None).await;
1082
    }
1083

1084 1
    #[actix_rt::test]
1085 1
    async fn test_identity_cookie_not_updated_on_login_deadline() {
1086 1
        let mut srv =
1087
            create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
1088 1
        let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
1089 1
        let mut resp = test::call_service(
1090
            &mut srv,
1091 1
            TestRequest::with_uri("/")
1092 1
                .cookie(cookie.clone())
1093
                .to_request(),
1094
        )
1095
        .await;
1096 1
        assert_no_login_cookie(&mut resp);
1097 1
        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
1098
    }
1099

1100
    // https://github.com/actix/actix-web/issues/1263
1101 1
    #[actix_rt::test]
1102 1
    async fn test_identity_cookie_updated_on_visit_deadline() {
1103 1
        let mut srv = create_identity_server(|c| {
1104 1
            c.visit_deadline(Duration::days(90))
1105 1
                .login_deadline(Duration::days(90))
1106
        })
1107
        .await;
1108 1
        let timestamp = SystemTime::now() - Duration::days(1);
1109 1
        let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
1110 1
        let mut resp = test::call_service(
1111
            &mut srv,
1112 1
            TestRequest::with_uri("/")
1113 1
                .cookie(cookie.clone())
1114
                .to_request(),
1115
        )
1116
        .await;
1117
        assert_login_cookie(
1118 1
            &mut resp,
1119
            COOKIE_LOGIN,
1120 1
            LoginTimestampCheck::OldTimestamp(timestamp),
1121 1
            VisitTimeStampCheck::NewTimestamp,
1122
        );
1123 1
        assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
1124
    }
1125

1126 1
    #[actix_rt::test]
1127 1
    async fn test_borrowed_mut_error() {
1128
        use futures_util::future::{lazy, ok, Ready};
1129

1130
        struct Ident;
1131
        impl IdentityPolicy for Ident {
1132
            type Future = Ready<Result<Option<String>, Error>>;
1133
            type ResponseFuture = Ready<Result<(), Error>>;
1134

1135 1
            fn from_request(&self, _: &mut ServiceRequest) -> Self::Future {
1136 1
                ok(Some("test".to_string()))
1137
            }
1138

1139 0
            fn to_response<B>(
1140
                &self,
1141
                _: Option<String>,
1142
                _: bool,
1143
                _: &mut ServiceResponse<B>,
1144
            ) -> Self::ResponseFuture {
1145 0
                ok(())
1146
            }
1147
        }
1148

1149
        let mut srv = IdentityServiceMiddleware {
1150 1
            backend: Rc::new(Ident),
1151 1
            service: Rc::new(RefCell::new(into_service(
1152
                |_: ServiceRequest| async move {
1153
                    actix_rt::time::delay_for(std::time::Duration::from_secs(100)).await;
1154
                    Err::<ServiceResponse, _>(error::ErrorBadRequest("error"))
1155
                },
1156
            ))),
1157
        };
1158

1159 1
        let mut srv2 = srv.clone();
1160 1
        let req = TestRequest::default().to_srv_request();
1161 1
        actix_rt::spawn(async move {
1162 1
            let _ = srv2.call(req).await;
1163
        });
1164 1
        actix_rt::time::delay_for(std::time::Duration::from_millis(50)).await;
1165

1166 1
        let _ = lazy(|cx| srv.poll_ready(cx)).await;
1167
    }
1168
}

Read our documentation on viewing source code .

Loading