1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-28 01:52:57 +01:00
actix-web/src/middleware/identity.rs

1038 lines
34 KiB
Rust
Raw Normal View History

2019-03-10 03:04:40 +01:00
//! Request identity service for Actix applications.
//!
//! [**IdentityService**](struct.IdentityService.html) middleware can be
//! used with different policies types to store identity information.
//!
//! By default, only cookie identity policy is implemented. Other backend
//! implementations can be added separately.
//!
//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html)
//! uses cookies as identity storage.
//!
//! To access current request identity
2019-03-10 05:48:05 +01:00
//! [**Identity**](trait.Identity.html) extractor should be used.
2019-03-10 03:04:40 +01:00
//!
//! ```rust
2019-03-10 05:40:09 +01:00
//! use actix_web::middleware::identity::Identity;
2019-03-10 03:04:40 +01:00
//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
//! use actix_web::*;
//!
2019-03-10 05:40:09 +01:00
//! fn index(id: Identity) -> String {
2019-03-10 03:04:40 +01:00
//! // access request identity
2019-03-10 05:40:09 +01:00
//! if let Some(id) = id.identity() {
//! format!("Welcome! {}", id)
2019-03-10 03:04:40 +01:00
//! } else {
2019-03-10 05:40:09 +01:00
//! "Welcome Anonymous!".to_owned()
2019-03-10 03:04:40 +01:00
//! }
//! }
//!
2019-03-10 06:15:26 +01:00
//! fn login(id: Identity) -> HttpResponse {
2019-03-10 05:40:09 +01:00
//! id.remember("User1".to_owned()); // <- remember identity
2019-03-10 03:04:40 +01:00
//! HttpResponse::Ok().finish()
//! }
//!
2019-03-10 05:40:09 +01:00
//! fn logout(id: Identity) -> HttpResponse {
//! id.forget(); // <- remove identity
2019-03-10 03:04:40 +01:00
//! HttpResponse::Ok().finish()
//! }
//!
//! fn main() {
2019-03-25 21:02:10 +01:00
//! let app = App::new().wrap(IdentityService::new(
2019-03-10 03:04:40 +01:00
//! // <- create identity middleware
//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend
//! .name("auth-cookie")
2019-03-10 06:15:26 +01:00
//! .secure(false)))
//! .service(web::resource("/index.html").to(index))
//! .service(web::resource("/login.html").to(login))
//! .service(web::resource("/logout.html").to(logout));
2019-03-10 03:04:40 +01:00
//! }
//! ```
2019-03-10 05:40:09 +01:00
use std::cell::RefCell;
2019-03-10 03:04:40 +01:00
use std::rc::Rc;
use std::time::SystemTime;
2019-03-10 03:04:40 +01:00
2019-03-10 05:40:09 +01:00
use actix_service::{Service, Transform};
use futures::future::{ok, Either, FutureResult};
use futures::{Future, IntoFuture, Poll};
use serde::{Deserialize, Serialize};
2019-04-20 02:23:17 +02:00
use time::Duration;
2019-03-10 03:04:40 +01:00
2019-03-30 05:13:39 +01:00
use crate::cookie::{Cookie, CookieJar, Key, SameSite};
2019-03-10 05:40:09 +01:00
use crate::error::{Error, Result};
use crate::http::header::{self, HeaderValue};
2019-04-07 23:43:07 +02:00
use crate::service::{ServiceRequest, ServiceResponse};
use crate::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
2019-03-10 03:04:40 +01:00
2019-03-10 05:40:09 +01:00
/// The extractor type to obtain your identity from a request.
2019-03-10 03:04:40 +01:00
///
/// ```rust
/// use actix_web::*;
2019-03-10 05:40:09 +01:00
/// use actix_web::middleware::identity::Identity;
2019-03-10 03:04:40 +01:00
///
2019-03-10 05:40:09 +01:00
/// fn index(id: Identity) -> Result<String> {
2019-03-10 03:04:40 +01:00
/// // access request identity
2019-03-10 05:40:09 +01:00
/// if let Some(id) = id.identity() {
2019-03-10 03:04:40 +01:00
/// Ok(format!("Welcome! {}", id))
/// } else {
/// Ok("Welcome Anonymous!".to_owned())
/// }
/// }
///
2019-03-10 05:40:09 +01:00
/// fn login(id: Identity) -> HttpResponse {
/// id.remember("User1".to_owned()); // <- remember identity
2019-03-10 03:04:40 +01:00
/// HttpResponse::Ok().finish()
/// }
///
2019-03-10 05:40:09 +01:00
/// fn logout(id: Identity) -> HttpResponse {
/// id.forget(); // <- remove identity
2019-03-10 03:04:40 +01:00
/// HttpResponse::Ok().finish()
/// }
/// # fn main() {}
/// ```
2019-03-10 05:40:09 +01:00
#[derive(Clone)]
pub struct Identity(HttpRequest);
impl Identity {
2019-03-10 03:04:40 +01:00
/// Return the claimed identity of the user associated request or
/// ``None`` if no identity can be found associated with the request.
2019-03-10 05:40:09 +01:00
pub fn identity(&self) -> Option<String> {
if let Some(id) = self.0.extensions().get::<IdentityItem>() {
id.id.clone()
} else {
None
2019-03-10 03:04:40 +01:00
}
}
2019-03-10 05:40:09 +01:00
/// Remember identity.
pub fn remember(&self, identity: String) {
if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
id.id = Some(identity);
id.changed = true;
2019-03-10 03:04:40 +01:00
}
}
2019-03-10 05:40:09 +01:00
/// This method is used to 'forget' the current identity on subsequent
/// requests.
pub fn forget(&self) {
if let Some(id) = self.0.extensions_mut().get_mut::<IdentityItem>() {
id.id = None;
id.changed = true;
2019-03-10 03:04:40 +01:00
}
}
}
2019-03-10 05:40:09 +01:00
struct IdentityItem {
id: Option<String>,
changed: bool,
}
2019-03-10 03:04:40 +01:00
2019-03-10 05:40:09 +01:00
/// Extractor implementation for Identity type.
///
/// ```rust
/// # use actix_web::*;
/// use actix_web::middleware::identity::Identity;
///
/// fn index(id: Identity) -> String {
/// // access request identity
/// if let Some(id) = id.identity() {
/// format!("Welcome! {}", id)
/// } else {
/// "Welcome Anonymous!".to_owned()
/// }
/// }
/// # fn main() {}
/// ```
impl FromRequest for Identity {
2019-04-14 01:35:25 +02:00
type Config = ();
2019-03-10 05:40:09 +01:00
type Error = Error;
type Future = Result<Identity, Error>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
2019-04-07 23:43:07 +02:00
Ok(Identity(req.clone()))
2019-03-10 05:40:09 +01:00
}
2019-03-10 03:04:40 +01:00
}
/// Identity policy definition.
2019-03-10 05:40:09 +01:00
pub trait IdentityPolicy: Sized + 'static {
/// The return type of the middleware
type Future: IntoFuture<Item = Option<String>, Error = Error>;
2019-03-10 03:04:40 +01:00
/// The return type of the middleware
2019-03-10 05:40:09 +01:00
type ResponseFuture: IntoFuture<Item = (), Error = Error>;
2019-03-10 03:04:40 +01:00
/// Parse the session from request and load data from a service identity.
fn from_request(&self, request: &mut ServiceRequest) -> Self::Future;
2019-03-10 05:40:09 +01:00
/// Write changes to response
fn to_response<B>(
&self,
identity: Option<String>,
changed: bool,
response: &mut ServiceResponse<B>,
) -> Self::ResponseFuture;
2019-03-10 03:04:40 +01:00
}
/// Request identity middleware
///
/// ```rust
/// use actix_web::App;
2019-03-10 05:40:09 +01:00
/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
2019-03-10 03:04:40 +01:00
///
/// fn main() {
2019-03-25 21:02:10 +01:00
/// let app = App::new().wrap(IdentityService::new(
2019-03-10 03:04:40 +01:00
/// // <- create identity middleware
/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend
/// .name("auth-cookie")
/// .secure(false),
/// ));
/// }
/// ```
pub struct IdentityService<T> {
2019-03-10 05:40:09 +01:00
backend: Rc<T>,
2019-03-10 03:04:40 +01:00
}
impl<T> IdentityService<T> {
/// Create new identity service with specified backend.
pub fn new(backend: T) -> Self {
2019-03-10 05:40:09 +01:00
IdentityService {
backend: Rc::new(backend),
}
2019-03-10 03:04:40 +01:00
}
}
impl<S, T, B> Transform<S> for IdentityService<T>
2019-03-10 05:40:09 +01:00
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>> + 'static,
2019-03-10 05:40:09 +01:00
S::Future: 'static,
2019-04-04 19:59:34 +02:00
S::Error: 'static,
2019-03-10 05:40:09 +01:00
T: IdentityPolicy,
B: 'static,
{
type Request = ServiceRequest;
2019-03-10 05:40:09 +01:00
type Response = ServiceResponse<B>;
type Error = S::Error;
type InitError = ();
type Transform = IdentityServiceMiddleware<S, T>;
type Future = FutureResult<Self::Transform, Self::InitError>;
fn new_transform(&self, service: S) -> Self::Future {
ok(IdentityServiceMiddleware {
backend: self.backend.clone(),
service: Rc::new(RefCell::new(service)),
})
2019-03-10 03:04:40 +01:00
}
}
2019-03-10 05:48:05 +01:00
#[doc(hidden)]
2019-03-10 05:40:09 +01:00
pub struct IdentityServiceMiddleware<S, T> {
backend: Rc<T>,
service: Rc<RefCell<S>>,
2019-03-10 03:04:40 +01:00
}
impl<S, T, B> Service for IdentityServiceMiddleware<S, T>
2019-03-10 05:40:09 +01:00
where
B: 'static,
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>> + 'static,
2019-03-10 05:40:09 +01:00
S::Future: 'static,
2019-04-04 19:59:34 +02:00
S::Error: 'static,
2019-03-10 05:40:09 +01:00
T: IdentityPolicy,
{
type Request = ServiceRequest;
2019-03-10 05:40:09 +01:00
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
self.service.borrow_mut().poll_ready()
2019-03-10 03:04:40 +01:00
}
fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
2019-03-10 05:40:09 +01:00
let srv = self.service.clone();
let backend = self.backend.clone();
Box::new(
self.backend.from_request(&mut req).into_future().then(
move |res| match res {
Ok(id) => {
req.extensions_mut()
.insert(IdentityItem { id, changed: false });
Either::A(srv.borrow_mut().call(req).and_then(move |mut res| {
let id =
res.request().extensions_mut().remove::<IdentityItem>();
if let Some(id) = id {
return Either::A(
backend
.to_response(id.id, id.changed, &mut res)
.into_future()
.then(move |t| match t {
Ok(_) => Ok(res),
Err(e) => Ok(res.error_response(e)),
}),
);
} else {
Either::B(ok(res))
}
}))
}
Err(err) => Either::B(ok(req.error_response(err))),
},
),
)
2019-03-10 03:04:40 +01:00
}
}
struct CookieIdentityInner {
key: Key,
key_v2: Key,
2019-03-10 03:04:40 +01:00
name: String,
path: String,
domain: Option<String>,
secure: bool,
max_age: Option<Duration>,
same_site: Option<SameSite>,
visit_deadline: Option<Duration>,
login_deadline: Option<Duration>,
}
#[derive(Deserialize, Serialize, Debug)]
struct CookieValue {
identity: String,
#[serde(skip_serializing_if = "Option::is_none")]
login_timestamp: Option<SystemTime>,
#[serde(skip_serializing_if = "Option::is_none")]
visit_timestamp: Option<SystemTime>,
}
#[derive(Debug)]
struct CookieIdentityExtention {
2019-04-25 00:29:15 +02:00
login_timestamp: Option<SystemTime>,
2019-03-10 03:04:40 +01:00
}
impl CookieIdentityInner {
fn new(key: &[u8]) -> CookieIdentityInner {
2019-04-25 00:29:15 +02:00
let key_v2: Vec<u8> =
key.iter().chain([1, 0, 0, 0].iter()).map(|e| *e).collect();
2019-03-10 03:04:40 +01:00
CookieIdentityInner {
key: Key::from_master(key),
key_v2: Key::from_master(&key_v2),
2019-03-10 03:04:40 +01:00
name: "actix-identity".to_owned(),
path: "/".to_owned(),
domain: None,
secure: true,
max_age: None,
same_site: None,
visit_deadline: None,
login_deadline: None,
2019-03-10 03:04:40 +01:00
}
}
2019-03-10 05:40:09 +01:00
fn set_cookie<B>(
&self,
resp: &mut ServiceResponse<B>,
value: Option<CookieValue>,
2019-03-10 05:40:09 +01:00
) -> Result<()> {
let add_cookie = value.is_some();
2019-04-25 00:29:15 +02:00
let val = value.map(|val| {
if !self.legacy_supported() {
serde_json::to_string(&val)
} else {
Ok(val.identity)
}
});
2019-04-25 00:29:15 +02:00
let mut cookie =
Cookie::new(self.name.clone(), val.unwrap_or_else(|| Ok(String::new()))?);
cookie.set_path(self.path.clone());
cookie.set_secure(self.secure);
cookie.set_http_only(true);
if let Some(ref domain) = self.domain {
cookie.set_domain(domain.clone());
}
2019-03-10 03:04:40 +01:00
if let Some(max_age) = self.max_age {
cookie.set_max_age(max_age);
}
2019-03-10 03:04:40 +01:00
if let Some(same_site) = self.same_site {
cookie.set_same_site(same_site);
2019-03-10 03:04:40 +01:00
}
let mut jar = CookieJar::new();
2019-04-25 00:29:15 +02:00
let key = if self.legacy_supported() {
&self.key
} else {
&self.key_v2
};
if add_cookie {
jar.private(&key).add(cookie);
} else {
jar.add_original(cookie.clone());
jar.private(&key).remove(cookie);
}
for cookie in jar.delta() {
let val = HeaderValue::from_str(&cookie.to_string())?;
resp.headers_mut().append(header::SET_COOKIE, val);
}
2019-03-10 03:04:40 +01:00
Ok(())
}
fn load(&self, req: &ServiceRequest) -> Option<CookieValue> {
let cookie = req.cookie(&self.name)?;
let mut jar = CookieJar::new();
jar.add_original(cookie.clone());
let res = if self.legacy_supported() {
jar.private(&self.key).get(&self.name).map(|n| CookieValue {
identity: n.value().to_string(),
login_timestamp: None,
2019-04-25 00:29:15 +02:00
visit_timestamp: None,
})
} else {
None
};
2019-04-25 00:29:15 +02:00
res.or_else(|| {
jar.private(&self.key_v2)
.get(&self.name)
.and_then(|c| self.parse(c))
})
}
2019-03-10 03:04:40 +01:00
fn parse(&self, cookie: Cookie) -> Option<CookieValue> {
let value: CookieValue = serde_json::from_str(cookie.value()).ok()?;
let now = SystemTime::now();
if let Some(visit_deadline) = self.visit_deadline {
2019-04-25 00:29:15 +02:00
if now.duration_since(value.visit_timestamp?).ok()?
> visit_deadline.to_std().ok()?
{
return None;
2019-03-10 03:04:40 +01:00
}
}
if let Some(login_deadline) = self.login_deadline {
2019-04-25 00:29:15 +02:00
if now.duration_since(value.login_timestamp?).ok()?
> login_deadline.to_std().ok()?
{
return None;
}
}
Some(value)
}
fn legacy_supported(&self) -> bool {
self.visit_deadline.is_none() && self.login_deadline.is_none()
}
fn always_update_cookie(&self) -> bool {
self.visit_deadline.is_some()
}
fn requires_oob_data(&self) -> bool {
self.login_deadline.is_some()
2019-03-10 03:04:40 +01:00
}
}
/// Use cookies for request identity storage.
///
/// The constructors take a key as an argument.
/// This is the private key for cookie - when this value is changed,
/// all identities are lost. The constructors will panic if the key is less
/// than 32 bytes in length.
///
/// # Example
///
/// ```rust
/// # extern crate actix_web;
/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
/// use actix_web::App;
///
/// fn main() {
2019-03-25 21:02:10 +01:00
/// let app = App::new().wrap(IdentityService::new(
2019-03-10 03:04:40 +01:00
/// // <- create identity middleware
/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy
/// .domain("www.rust-lang.org")
/// .name("actix_auth")
/// .path("/")
/// .secure(true),
/// ));
/// }
/// ```
pub struct CookieIdentityPolicy(Rc<CookieIdentityInner>);
impl CookieIdentityPolicy {
/// Construct new `CookieIdentityPolicy` instance.
///
/// Panics if key length is less than 32 bytes.
pub fn new(key: &[u8]) -> CookieIdentityPolicy {
CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key)))
}
/// Sets the `path` field in the session cookie being built.
pub fn path<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().path = value.into();
self
}
/// Sets the `name` field in the session cookie being built.
pub fn name<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().name = value.into();
self
}
/// Sets the `domain` field in the session cookie being built.
pub fn domain<S: Into<String>>(mut self, value: S) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into());
self
}
/// Sets the `secure` field in the session cookie being built.
///
/// If the `secure` field is set, a cookie will only be transmitted when the
/// connection is secure - i.e. `https`
pub fn secure(mut self, value: bool) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().secure = value;
self
}
2019-04-20 02:23:17 +02:00
/// Sets the `max-age` field in the session cookie being built with given number of seconds.
pub fn max_age(self, seconds: i64) -> CookieIdentityPolicy {
self.max_age_time(Duration::seconds(seconds))
}
/// Sets the `max-age` field in the session cookie being built with `chrono::Duration`.
pub fn max_age_time(mut self, value: Duration) -> CookieIdentityPolicy {
2019-03-10 03:04:40 +01:00
Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
self
}
/// Sets the `same_site` field in the session cookie being built.
pub fn same_site(mut self, same_site: SameSite) -> Self {
Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site);
self
}
/// Accepts only users whose cookie has been seen before the given deadline
2019-04-25 00:42:34 +02:00
///
/// By default visit deadline is disabled.
pub fn visit_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().visit_deadline = Some(value);
self
}
/// Accepts only users which has been authenticated before the given deadline
2019-04-25 00:42:34 +02:00
///
/// By default login deadline is disabled.
pub fn login_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
Rc::get_mut(&mut self.0).unwrap().login_deadline = Some(value);
self
}
2019-03-10 03:04:40 +01:00
}
2019-03-10 05:40:09 +01:00
impl IdentityPolicy for CookieIdentityPolicy {
type Future = Result<Option<String>, Error>;
type ResponseFuture = Result<(), Error>;
2019-03-10 03:04:40 +01:00
fn from_request(&self, req: &mut ServiceRequest) -> Self::Future {
2019-04-25 00:29:15 +02:00
Ok(self.0.load(req).map(
|CookieValue {
identity,
login_timestamp,
..
}| {
if self.0.requires_oob_data() {
req.extensions_mut()
.insert(CookieIdentityExtention { login_timestamp });
}
identity
},
))
2019-03-10 05:40:09 +01:00
}
fn to_response<B>(
&self,
id: Option<String>,
changed: bool,
res: &mut ServiceResponse<B>,
) -> Self::ResponseFuture {
let _ = if changed {
let login_timestamp = SystemTime::now();
2019-04-25 00:29:15 +02:00
self.0.set_cookie(
res,
id.map(|identity| CookieValue {
identity,
login_timestamp: self.0.login_deadline.map(|_| login_timestamp),
visit_timestamp: self.0.visit_deadline.map(|_| login_timestamp),
}),
)
} else if self.0.always_update_cookie() && id.is_some() {
let visit_timestamp = SystemTime::now();
let mut login_timestamp = None;
if self.0.requires_oob_data() {
2019-04-25 00:29:15 +02:00
let CookieIdentityExtention {
login_timestamp: lt,
} = res.request().extensions_mut().remove().unwrap();
login_timestamp = lt;
}
2019-04-25 00:29:15 +02:00
self.0.set_cookie(
res,
Some(CookieValue {
identity: id.unwrap(),
login_timestamp,
visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp),
}),
)
} else {
Ok(())
};
2019-03-10 05:40:09 +01:00
Ok(())
2019-03-10 03:04:40 +01:00
}
}
2019-03-11 01:10:41 +01:00
#[cfg(test)]
mod tests {
use super::*;
use crate::http::StatusCode;
use crate::test::{self, TestRequest};
use crate::{web, App, HttpResponse};
use std::borrow::Borrow;
const COOKIE_KEY_MASTER: [u8; 32] = [0; 32];
const COOKIE_NAME: &'static str = "actix_auth";
const COOKIE_LOGIN: &'static str = "test";
2019-03-11 01:10:41 +01:00
#[test]
fn test_identity() {
let mut srv = test::init_service(
App::new()
2019-03-25 21:02:10 +01:00
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
2019-03-11 01:10:41 +01:00
.domain("www.rust-lang.org")
.name(COOKIE_NAME)
2019-03-11 01:10:41 +01:00
.path("/")
.secure(true),
))
.service(web::resource("/index").to(|id: Identity| {
if id.identity().is_some() {
HttpResponse::Created()
} else {
HttpResponse::Ok()
}
}))
.service(web::resource("/login").to(|id: Identity| {
id.remember(COOKIE_LOGIN.to_string());
2019-03-11 01:10:41 +01:00
HttpResponse::Ok()
}))
.service(web::resource("/logout").to(|id: Identity| {
if id.identity().is_some() {
id.forget();
HttpResponse::Ok()
} else {
HttpResponse::BadRequest()
}
})),
);
let resp =
test::call_service(&mut srv, TestRequest::with_uri("/index").to_request());
2019-03-11 01:10:41 +01:00
assert_eq!(resp.status(), StatusCode::OK);
let resp =
test::call_service(&mut srv, TestRequest::with_uri("/login").to_request());
2019-03-11 01:10:41 +01:00
assert_eq!(resp.status(), StatusCode::OK);
2019-04-02 22:35:01 +02:00
let c = resp.response().cookies().next().unwrap().to_owned();
2019-03-11 01:10:41 +01:00
let resp = test::call_service(
2019-03-11 01:10:41 +01:00
&mut srv,
TestRequest::with_uri("/index")
.cookie(c.clone())
.to_request(),
);
assert_eq!(resp.status(), StatusCode::CREATED);
let resp = test::call_service(
2019-03-11 01:10:41 +01:00
&mut srv,
TestRequest::with_uri("/logout")
.cookie(c.clone())
.to_request(),
);
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().contains_key(header::SET_COOKIE))
}
#[test]
fn test_identity_max_age_time() {
let duration = Duration::days(1);
let mut srv = test::init_service(
App::new()
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
.domain("www.rust-lang.org")
.name(COOKIE_NAME)
.path("/")
.max_age_time(duration)
.secure(true),
))
.service(web::resource("/login").to(|id: Identity| {
id.remember("test".to_string());
HttpResponse::Ok()
2019-04-20 02:23:17 +02:00
})),
);
let resp =
test::call_service(&mut srv, TestRequest::with_uri("/login").to_request());
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().contains_key(header::SET_COOKIE));
let c = resp.response().cookies().next().unwrap().to_owned();
assert_eq!(duration, c.max_age().unwrap());
}
#[test]
fn test_identity_max_age() {
2019-04-20 02:23:17 +02:00
let seconds = 60;
let mut srv = test::init_service(
App::new()
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
.domain("www.rust-lang.org")
.name(COOKIE_NAME)
.path("/")
.max_age(seconds)
.secure(true),
))
.service(web::resource("/login").to(|id: Identity| {
id.remember("test".to_string());
HttpResponse::Ok()
2019-04-20 02:23:17 +02:00
})),
);
let resp =
test::call_service(&mut srv, TestRequest::with_uri("/login").to_request());
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().contains_key(header::SET_COOKIE));
let c = resp.response().cookies().next().unwrap().to_owned();
assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap());
}
2019-04-25 00:29:15 +02:00
fn create_identity_server<
F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static,
>(
f: F,
) -> impl actix_service::Service<
Request = actix_http::Request,
Response = ServiceResponse<actix_http::body::Body>,
Error = actix_http::Error,
> {
test::init_service(
App::new()
2019-04-25 00:29:15 +02:00
.wrap(IdentityService::new(f(CookieIdentityPolicy::new(
&COOKIE_KEY_MASTER,
)
.secure(false)
.name(COOKIE_NAME))))
.service(web::resource("/").to(|id: Identity| {
let identity = id.identity();
if identity.is_none() {
id.remember(COOKIE_LOGIN.to_string())
}
web::Json(identity)
2019-04-25 00:29:15 +02:00
})),
)
}
fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> {
let mut jar = CookieJar::new();
2019-04-25 00:29:15 +02:00
jar.private(&Key::from_master(&COOKIE_KEY_MASTER))
.add(Cookie::new(COOKIE_NAME, identity));
jar.get(COOKIE_NAME).unwrap().clone()
}
2019-04-25 00:29:15 +02:00
fn login_cookie(
identity: &'static str,
login_timestamp: Option<SystemTime>,
visit_timestamp: Option<SystemTime>,
) -> Cookie<'static> {
let mut jar = CookieJar::new();
2019-04-25 00:29:15 +02:00
let key: Vec<u8> = COOKIE_KEY_MASTER
.iter()
.chain([1, 0, 0, 0].iter())
.map(|e| *e)
.collect();
jar.private(&Key::from_master(&key)).add(Cookie::new(
COOKIE_NAME,
serde_json::to_string(&CookieValue {
identity: identity.to_string(),
login_timestamp,
visit_timestamp,
})
.unwrap(),
));
jar.get(COOKIE_NAME).unwrap().clone()
}
fn assert_logged_in(response: &mut ServiceResponse, identity: Option<&str>) {
use bytes::BytesMut;
use futures::Stream;
let bytes =
test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| {
b.extend(c);
Ok::<_, Error>(b)
}))
.unwrap();
let resp: Option<String> = serde_json::from_slice(&bytes[..]).unwrap();
assert_eq!(resp.as_ref().map(|s| s.borrow()), identity);
}
fn assert_legacy_login_cookie(response: &mut ServiceResponse, identity: &str) {
let mut cookies = CookieJar::new();
for cookie in response.headers().get_all(header::SET_COOKIE) {
cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
}
2019-04-25 00:29:15 +02:00
let cookie = cookies
.private(&Key::from_master(&COOKIE_KEY_MASTER))
.get(COOKIE_NAME)
.unwrap();
assert_eq!(cookie.value(), identity);
}
enum LoginTimestampCheck {
NoTimestamp,
NewTimestamp,
2019-04-25 00:29:15 +02:00
OldTimestamp(SystemTime),
}
enum VisitTimeStampCheck {
NoTimestamp,
2019-04-25 00:29:15 +02:00
NewTimestamp,
}
2019-04-25 00:29:15 +02:00
fn assert_login_cookie(
response: &mut ServiceResponse,
identity: &str,
login_timestamp: LoginTimestampCheck,
visit_timestamp: VisitTimeStampCheck,
) {
let mut cookies = CookieJar::new();
for cookie in response.headers().get_all(header::SET_COOKIE) {
cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
}
2019-04-25 00:29:15 +02:00
let key: Vec<u8> = COOKIE_KEY_MASTER
.iter()
.chain([1, 0, 0, 0].iter())
.map(|e| *e)
.collect();
let cookie = cookies
.private(&Key::from_master(&key))
.get(COOKIE_NAME)
.unwrap();
let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap();
assert_eq!(cv.identity, identity);
let now = SystemTime::now();
let t30sec_ago = now - Duration::seconds(30).to_std().unwrap();
match login_timestamp {
LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None),
2019-04-25 00:29:15 +02:00
LoginTimestampCheck::NewTimestamp => assert!(
t30sec_ago <= cv.login_timestamp.unwrap()
&& cv.login_timestamp.unwrap() <= now
),
LoginTimestampCheck::OldTimestamp(old_timestamp) => {
assert_eq!(cv.login_timestamp, Some(old_timestamp))
}
}
match visit_timestamp {
VisitTimeStampCheck::NoTimestamp => assert_eq!(cv.visit_timestamp, None),
2019-04-25 00:29:15 +02:00
VisitTimeStampCheck::NewTimestamp => assert!(
t30sec_ago <= cv.visit_timestamp.unwrap()
&& cv.visit_timestamp.unwrap() <= now
),
}
}
fn assert_no_login_cookie(response: &mut ServiceResponse) {
let mut cookies = CookieJar::new();
for cookie in response.headers().get_all(header::SET_COOKIE) {
cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
}
assert!(cookies.get(COOKIE_NAME).is_none());
}
#[test]
fn test_identity_legacy_cookie_is_set() {
let mut srv = create_identity_server(|c| c);
2019-04-25 00:29:15 +02:00
let mut resp =
test::call_service(&mut srv, TestRequest::with_uri("/").to_request());
assert_logged_in(&mut resp, None);
assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN);
}
#[test]
fn test_identity_legacy_cookie_works() {
let mut srv = create_identity_server(|c| c);
let cookie = legacy_login_cookie(COOKIE_LOGIN);
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, Some(COOKIE_LOGIN));
assert_no_login_cookie(&mut resp);
}
#[test]
fn test_identity_legacy_cookie_rejected_if_visit_timestamp_needed() {
let mut srv = create_identity_server(|c| c.visit_deadline(Duration::days(90)));
let cookie = legacy_login_cookie(COOKIE_LOGIN);
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, None);
2019-04-25 00:29:15 +02:00
assert_login_cookie(
&mut resp,
COOKIE_LOGIN,
LoginTimestampCheck::NoTimestamp,
VisitTimeStampCheck::NewTimestamp,
);
}
#[test]
fn test_identity_legacy_cookie_rejected_if_login_timestamp_needed() {
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
let cookie = legacy_login_cookie(COOKIE_LOGIN);
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, None);
2019-04-25 00:29:15 +02:00
assert_login_cookie(
&mut resp,
COOKIE_LOGIN,
LoginTimestampCheck::NewTimestamp,
VisitTimeStampCheck::NoTimestamp,
);
}
#[test]
fn test_identity_cookie_rejected_if_login_timestamp_needed() {
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now()));
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, None);
2019-04-25 00:29:15 +02:00
assert_login_cookie(
&mut resp,
COOKIE_LOGIN,
LoginTimestampCheck::NewTimestamp,
VisitTimeStampCheck::NoTimestamp,
);
}
#[test]
fn test_identity_cookie_rejected_if_visit_timestamp_needed() {
let mut srv = create_identity_server(|c| c.visit_deadline(Duration::days(90)));
let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, None);
2019-04-25 00:29:15 +02:00
assert_login_cookie(
&mut resp,
COOKIE_LOGIN,
LoginTimestampCheck::NoTimestamp,
VisitTimeStampCheck::NewTimestamp,
);
}
#[test]
fn test_identity_cookie_rejected_if_login_timestamp_too_old() {
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
2019-04-25 00:29:15 +02:00
let cookie = login_cookie(
COOKIE_LOGIN,
Some(SystemTime::now() - Duration::days(180).to_std().unwrap()),
None,
);
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, None);
2019-04-25 00:29:15 +02:00
assert_login_cookie(
&mut resp,
COOKIE_LOGIN,
LoginTimestampCheck::NewTimestamp,
VisitTimeStampCheck::NoTimestamp,
);
}
#[test]
fn test_identity_cookie_rejected_if_visit_timestamp_too_old() {
let mut srv = create_identity_server(|c| c.visit_deadline(Duration::days(90)));
2019-04-25 00:29:15 +02:00
let cookie = login_cookie(
COOKIE_LOGIN,
None,
Some(SystemTime::now() - Duration::days(180).to_std().unwrap()),
);
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, None);
2019-04-25 00:29:15 +02:00
assert_login_cookie(
&mut resp,
COOKIE_LOGIN,
LoginTimestampCheck::NoTimestamp,
VisitTimeStampCheck::NewTimestamp,
);
}
#[test]
fn test_identity_cookie_not_updated_on_login_deadline() {
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, Some(COOKIE_LOGIN));
assert_no_login_cookie(&mut resp);
}
#[test]
fn test_identity_cookie_updated_on_visit_deadline() {
2019-04-25 00:29:15 +02:00
let mut srv = create_identity_server(|c| {
c.visit_deadline(Duration::days(90))
.login_deadline(Duration::days(90))
});
let timestamp = SystemTime::now() - Duration::days(1).to_std().unwrap();
let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
let mut resp = test::call_service(
&mut srv,
TestRequest::with_uri("/")
.cookie(cookie.clone())
2019-04-25 00:29:15 +02:00
.to_request(),
);
assert_logged_in(&mut resp, Some(COOKIE_LOGIN));
2019-04-25 00:29:15 +02:00
assert_login_cookie(
&mut resp,
COOKIE_LOGIN,
LoginTimestampCheck::OldTimestamp(timestamp),
VisitTimeStampCheck::NewTimestamp,
);
}
2019-03-11 01:10:41 +01:00
}