mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-23 23:51:06 +01:00
refactor identity (#168)
This commit is contained in:
parent
c7df62d0b6
commit
23912afd49
@ -1,12 +1,18 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
* Rename `CookieIdentityPolicy::{max_age => max_age_secs}`. [#168]
|
||||
* Rename `CookieIdentityPolicy::{max_age_time => max_age}`. [#168]
|
||||
* Update `actix-web` dependency to 4.0.0 beta.
|
||||
* Minimum supported Rust version (MSRV) is now 1.46.0.
|
||||
|
||||
[#168]: https://github.com/actix/actix-extras/pull/168
|
||||
|
||||
|
||||
## 0.3.1 - 2020-09-20
|
||||
* Add method to set HttpOnly flag on cookie identity. [#102]
|
||||
* Add method to set `HttpOnly` flag on cookie identity. [#102]
|
||||
|
||||
[#102]: https://github.com/actix/actix-extras/pull/102
|
||||
|
||||
|
||||
## 0.3.0 - 2020-09-11
|
||||
|
@ -16,13 +16,14 @@ name = "actix_identity"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.0.0-beta.4", default-features = false, features = ["cookies", "secure-cookies"] }
|
||||
actix-service = "2.0.0-beta.5"
|
||||
actix-web = { version = "4.0.0-beta.4", default-features = false, features = ["cookies", "secure-cookies"] }
|
||||
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
time = "0.2.23"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2"
|
||||
actix-http = "3.0.0-beta.4"
|
||||
actix-rt = "2"
|
||||
|
827
actix-identity/src/cookie.rs
Normal file
827
actix-identity/src/cookie.rs
Normal file
@ -0,0 +1,827 @@
|
||||
use std::{rc::Rc, time::SystemTime};
|
||||
|
||||
use futures_util::future::{ready, Ready};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::Duration;
|
||||
|
||||
use actix_web::{
|
||||
cookie::{Cookie, CookieJar, Key, SameSite},
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
error::{Error, Result},
|
||||
http::header::{self, HeaderValue},
|
||||
HttpMessage,
|
||||
};
|
||||
|
||||
use crate::IdentityPolicy;
|
||||
|
||||
struct CookieIdentityInner {
|
||||
key: Key,
|
||||
key_v2: Key,
|
||||
name: String,
|
||||
path: String,
|
||||
domain: Option<String>,
|
||||
secure: bool,
|
||||
max_age: Option<Duration>,
|
||||
http_only: Option<bool>,
|
||||
same_site: Option<SameSite>,
|
||||
visit_deadline: Option<Duration>,
|
||||
login_deadline: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
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 CookieIdentityExtension {
|
||||
login_timestamp: Option<SystemTime>,
|
||||
}
|
||||
|
||||
impl CookieIdentityInner {
|
||||
fn new(key: &[u8]) -> CookieIdentityInner {
|
||||
let key_v2: Vec<u8> = [key, &[1, 0, 0, 0]].concat();
|
||||
|
||||
CookieIdentityInner {
|
||||
key: Key::derive_from(key),
|
||||
key_v2: Key::derive_from(&key_v2),
|
||||
name: "actix-identity".to_owned(),
|
||||
path: "/".to_owned(),
|
||||
domain: None,
|
||||
secure: true,
|
||||
max_age: None,
|
||||
http_only: None,
|
||||
same_site: None,
|
||||
visit_deadline: None,
|
||||
login_deadline: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cookie<B>(
|
||||
&self,
|
||||
resp: &mut ServiceResponse<B>,
|
||||
value: Option<CookieValue>,
|
||||
) -> Result<()> {
|
||||
let add_cookie = value.is_some();
|
||||
let val = value.map(|val| {
|
||||
if !self.legacy_supported() {
|
||||
serde_json::to_string(&val)
|
||||
} else {
|
||||
Ok(val.identity)
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
if let Some(max_age) = self.max_age {
|
||||
cookie.set_max_age(max_age);
|
||||
}
|
||||
|
||||
if let Some(http_only) = self.http_only {
|
||||
cookie.set_http_only(http_only);
|
||||
}
|
||||
|
||||
if let Some(same_site) = self.same_site {
|
||||
cookie.set_same_site(same_site);
|
||||
}
|
||||
|
||||
let mut jar = CookieJar::new();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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,
|
||||
visit_timestamp: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
res.or_else(|| {
|
||||
jar.private(&self.key_v2)
|
||||
.get(&self.name)
|
||||
.and_then(|c| self.parse(c))
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
let inactivity = now.duration_since(value.visit_timestamp?).ok()?;
|
||||
|
||||
if inactivity > visit_deadline {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(login_deadline) = self.login_deadline {
|
||||
let logged_in_dur = now.duration_since(value.login_timestamp?).ok()?;
|
||||
|
||||
if logged_in_dur > login_deadline {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Use cookies for request identity storage.
|
||||
///
|
||||
/// [See this page on MDN](mdn-cookies) for details on cookie attributes.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::App;
|
||||
/// use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
///
|
||||
/// // create cookie identity backend
|
||||
/// let policy = CookieIdentityPolicy::new(&[0; 32])
|
||||
/// .domain("www.rust-lang.org")
|
||||
/// .name("actix_auth")
|
||||
/// .path("/")
|
||||
/// .secure(true);
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// // wrap policy into identity middleware
|
||||
/// .wrap(IdentityService::new(policy));
|
||||
/// ```
|
||||
///
|
||||
/// [mdn-cookies]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
|
||||
pub struct CookieIdentityPolicy(Rc<CookieIdentityInner>);
|
||||
|
||||
impl CookieIdentityPolicy {
|
||||
/// Create new `CookieIdentityPolicy` instance.
|
||||
///
|
||||
/// Key argument is the private key for issued cookies. If this value is changed, all issued
|
||||
/// cookie identities are invalidated.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `key` is less than 32 bytes in length..
|
||||
pub fn new(key: &[u8]) -> CookieIdentityPolicy {
|
||||
CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key)))
|
||||
}
|
||||
|
||||
/// Sets the name of issued cookies.
|
||||
pub fn name(mut self, value: impl Into<String>) -> CookieIdentityPolicy {
|
||||
self.inner_mut().name = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `Path` attribute of issued cookies.
|
||||
pub fn path(mut self, value: impl Into<String>) -> CookieIdentityPolicy {
|
||||
self.inner_mut().path = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `Domain` attribute of issued cookies.
|
||||
pub fn domain(mut self, value: impl Into<String>) -> CookieIdentityPolicy {
|
||||
self.inner_mut().domain = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `Secure` attribute of issued cookies.
|
||||
pub fn secure(mut self, value: bool) -> CookieIdentityPolicy {
|
||||
self.inner_mut().secure = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `Max-Age` attribute of issued cookies.
|
||||
pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy {
|
||||
self.inner_mut().max_age = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `Max-Age` attribute of issued cookies with given number of seconds.
|
||||
pub fn max_age_secs(self, seconds: i64) -> CookieIdentityPolicy {
|
||||
self.max_age(Duration::seconds(seconds))
|
||||
}
|
||||
|
||||
/// Sets the `HttpOnly` attribute of issued cookies.
|
||||
///
|
||||
/// By default, the `HttpOnly` attribute is omitted from issued cookies.
|
||||
pub fn http_only(mut self, http_only: bool) -> Self {
|
||||
self.inner_mut().http_only = Some(http_only);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `SameSite` attribute of issued cookies.
|
||||
///
|
||||
/// By default, the `SameSite` attribute is omitted from issued cookies.
|
||||
pub fn same_site(mut self, same_site: SameSite) -> Self {
|
||||
self.inner_mut().same_site = Some(same_site);
|
||||
self
|
||||
}
|
||||
|
||||
/// Accepts only users who have visited within given deadline.
|
||||
///
|
||||
/// In other words, invalidate a login after some amount of inactivity. Using this feature
|
||||
/// causes updated cookies to be issued on each response in order to record the user's last
|
||||
/// visitation timestamp.
|
||||
///
|
||||
/// By default, visit deadline is disabled.
|
||||
pub fn visit_deadline(mut self, deadline: Duration) -> CookieIdentityPolicy {
|
||||
self.inner_mut().visit_deadline = Some(deadline);
|
||||
self
|
||||
}
|
||||
|
||||
/// Accepts only users who authenticated within the given deadline.
|
||||
///
|
||||
/// In other words, invalidate a login after some amount of time, regardless of activity.
|
||||
/// While [`Max-Age`](CookieIdentityPolicy::max_age) is useful in constraining the cookie
|
||||
/// lifetime, it could be extended manually; using this feature encodes the deadline directly
|
||||
/// into the issued cookies, making it immutable to users.
|
||||
///
|
||||
/// By default, login deadline is disabled.
|
||||
pub fn login_deadline(mut self, deadline: Duration) -> CookieIdentityPolicy {
|
||||
self.inner_mut().login_deadline = Some(deadline);
|
||||
self
|
||||
}
|
||||
|
||||
fn inner_mut(&mut self) -> &mut CookieIdentityInner {
|
||||
Rc::get_mut(&mut self.0).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IdentityPolicy for CookieIdentityPolicy {
|
||||
type Future = Ready<Result<Option<String>, Error>>;
|
||||
type ResponseFuture = Ready<Result<(), Error>>;
|
||||
|
||||
fn from_request(&self, req: &mut ServiceRequest) -> Self::Future {
|
||||
ready(Ok(self.0.load(req).map(|value| {
|
||||
let CookieValue {
|
||||
identity,
|
||||
login_timestamp,
|
||||
..
|
||||
} = value;
|
||||
|
||||
if self.0.requires_oob_data() {
|
||||
req.extensions_mut()
|
||||
.insert(CookieIdentityExtension { login_timestamp });
|
||||
}
|
||||
|
||||
identity
|
||||
})))
|
||||
}
|
||||
|
||||
fn to_response<B>(
|
||||
&self,
|
||||
id: Option<String>,
|
||||
changed: bool,
|
||||
res: &mut ServiceResponse<B>,
|
||||
) -> Self::ResponseFuture {
|
||||
let _ = if changed {
|
||||
let login_timestamp = SystemTime::now();
|
||||
|
||||
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 login_timestamp = if self.0.requires_oob_data() {
|
||||
let CookieIdentityExtension { login_timestamp } =
|
||||
res.request().extensions_mut().remove().unwrap();
|
||||
|
||||
login_timestamp
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.0.set_cookie(
|
||||
res,
|
||||
Some(CookieValue {
|
||||
identity: id.unwrap(),
|
||||
login_timestamp,
|
||||
visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{borrow::Borrow, time::SystemTime};
|
||||
|
||||
use actix_web::{
|
||||
cookie::{Cookie, CookieJar, Key, SameSite},
|
||||
dev::ServiceResponse,
|
||||
http::{header, StatusCode},
|
||||
test::{self, TestRequest},
|
||||
web, App, HttpResponse,
|
||||
};
|
||||
use time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::{tests::*, Identity, IdentityService};
|
||||
|
||||
fn login_cookie(
|
||||
identity: &'static str,
|
||||
login_timestamp: Option<SystemTime>,
|
||||
visit_timestamp: Option<SystemTime>,
|
||||
) -> Cookie<'static> {
|
||||
let mut jar = CookieJar::new();
|
||||
let key: Vec<u8> = COOKIE_KEY_MASTER
|
||||
.iter()
|
||||
.chain([1, 0, 0, 0].iter())
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
jar.private(&Key::derive_from(&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_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());
|
||||
}
|
||||
|
||||
let key: Vec<u8> = COOKIE_KEY_MASTER
|
||||
.iter()
|
||||
.chain([1, 0, 0, 0].iter())
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let cookie = cookies
|
||||
.private(&Key::derive_from(&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);
|
||||
|
||||
match login_timestamp {
|
||||
LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None),
|
||||
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),
|
||||
VisitTimeStampCheck::NewTimestamp => assert!(
|
||||
t30sec_ago <= cv.visit_timestamp.unwrap()
|
||||
&& cv.visit_timestamp.unwrap() <= now
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_flow() {
|
||||
let srv = test::init_service(
|
||||
App::new()
|
||||
.wrap(IdentityService::new(
|
||||
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
|
||||
.domain("www.rust-lang.org")
|
||||
.name(COOKIE_NAME)
|
||||
.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());
|
||||
HttpResponse::Ok()
|
||||
}))
|
||||
.service(web::resource("/logout").to(|id: Identity| {
|
||||
if id.identity().is_some() {
|
||||
id.forget();
|
||||
HttpResponse::Ok()
|
||||
} else {
|
||||
HttpResponse::BadRequest()
|
||||
}
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let resp =
|
||||
test::call_service(&srv, TestRequest::with_uri("/index").to_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let resp =
|
||||
test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let c = resp.response().cookies().next().unwrap().to_owned();
|
||||
|
||||
let resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/index")
|
||||
.cookie(c.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
|
||||
let resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/logout")
|
||||
.cookie(c.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert!(resp.headers().contains_key(header::SET_COOKIE))
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_max_age_time() {
|
||||
let duration = Duration::days(1);
|
||||
|
||||
let 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(duration)
|
||||
.secure(true),
|
||||
))
|
||||
.service(web::resource("/login").to(|id: Identity| {
|
||||
id.remember("test".to_string());
|
||||
HttpResponse::Ok()
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
||||
let resp =
|
||||
test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
|
||||
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());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http_only_same_site() {
|
||||
let srv = test::init_service(
|
||||
App::new()
|
||||
.wrap(IdentityService::new(
|
||||
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
|
||||
.domain("www.rust-lang.org")
|
||||
.name(COOKIE_NAME)
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.same_site(SameSite::None),
|
||||
))
|
||||
.service(web::resource("/login").to(|id: Identity| {
|
||||
id.remember("test".to_string());
|
||||
HttpResponse::Ok()
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
||||
let resp =
|
||||
test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert!(resp.headers().contains_key(header::SET_COOKIE));
|
||||
|
||||
let c = resp.response().cookies().next().unwrap().to_owned();
|
||||
assert!(c.http_only().unwrap());
|
||||
assert_eq!(SameSite::None, c.same_site().unwrap());
|
||||
}
|
||||
|
||||
fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.private(&Key::derive_from(&COOKIE_KEY_MASTER))
|
||||
.add(Cookie::new(COOKIE_NAME, identity));
|
||||
jar.get(COOKIE_NAME).unwrap().clone()
|
||||
}
|
||||
|
||||
async fn assert_logged_in(response: ServiceResponse, identity: Option<&str>) {
|
||||
let bytes = test::read_body(response).await;
|
||||
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());
|
||||
}
|
||||
let cookie = cookies
|
||||
.private(&Key::derive_from(&COOKIE_KEY_MASTER))
|
||||
.get(COOKIE_NAME)
|
||||
.unwrap();
|
||||
assert_eq!(cookie.value(), identity);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_max_age() {
|
||||
let seconds = 60;
|
||||
let 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_secs(seconds)
|
||||
.secure(true),
|
||||
))
|
||||
.service(web::resource("/login").to(|id: Identity| {
|
||||
id.remember("test".to_string());
|
||||
HttpResponse::Ok()
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let resp =
|
||||
test::call_service(&srv, TestRequest::with_uri("/login").to_request()).await;
|
||||
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());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_legacy_cookie_is_set() {
|
||||
let srv = create_identity_server(|c| c).await;
|
||||
let mut resp =
|
||||
test::call_service(&srv, TestRequest::with_uri("/").to_request()).await;
|
||||
assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN);
|
||||
assert_logged_in(resp, None).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_legacy_cookie_works() {
|
||||
let srv = create_identity_server(|c| c).await;
|
||||
let cookie = legacy_login_cookie(COOKIE_LOGIN);
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_no_login_cookie(&mut resp);
|
||||
assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_legacy_cookie_rejected_if_visit_timestamp_needed() {
|
||||
let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
|
||||
let cookie = legacy_login_cookie(COOKIE_LOGIN);
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_login_cookie(
|
||||
&mut resp,
|
||||
COOKIE_LOGIN,
|
||||
LoginTimestampCheck::NoTimestamp,
|
||||
VisitTimeStampCheck::NewTimestamp,
|
||||
);
|
||||
assert_logged_in(resp, None).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_legacy_cookie_rejected_if_login_timestamp_needed() {
|
||||
let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
|
||||
let cookie = legacy_login_cookie(COOKIE_LOGIN);
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_login_cookie(
|
||||
&mut resp,
|
||||
COOKIE_LOGIN,
|
||||
LoginTimestampCheck::NewTimestamp,
|
||||
VisitTimeStampCheck::NoTimestamp,
|
||||
);
|
||||
assert_logged_in(resp, None).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_cookie_rejected_if_login_timestamp_needed() {
|
||||
let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
|
||||
let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now()));
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_login_cookie(
|
||||
&mut resp,
|
||||
COOKIE_LOGIN,
|
||||
LoginTimestampCheck::NewTimestamp,
|
||||
VisitTimeStampCheck::NoTimestamp,
|
||||
);
|
||||
assert_logged_in(resp, None).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_cookie_rejected_if_visit_timestamp_needed() {
|
||||
let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
|
||||
let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_login_cookie(
|
||||
&mut resp,
|
||||
COOKIE_LOGIN,
|
||||
LoginTimestampCheck::NoTimestamp,
|
||||
VisitTimeStampCheck::NewTimestamp,
|
||||
);
|
||||
assert_logged_in(resp, None).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_cookie_rejected_if_login_timestamp_too_old() {
|
||||
let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
|
||||
let cookie = login_cookie(
|
||||
COOKIE_LOGIN,
|
||||
Some(SystemTime::now() - Duration::days(180)),
|
||||
None,
|
||||
);
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_login_cookie(
|
||||
&mut resp,
|
||||
COOKIE_LOGIN,
|
||||
LoginTimestampCheck::NewTimestamp,
|
||||
VisitTimeStampCheck::NoTimestamp,
|
||||
);
|
||||
assert_logged_in(resp, None).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_cookie_rejected_if_visit_timestamp_too_old() {
|
||||
let srv = create_identity_server(|c| c.visit_deadline(Duration::days(90))).await;
|
||||
let cookie = login_cookie(
|
||||
COOKIE_LOGIN,
|
||||
None,
|
||||
Some(SystemTime::now() - Duration::days(180)),
|
||||
);
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_login_cookie(
|
||||
&mut resp,
|
||||
COOKIE_LOGIN,
|
||||
LoginTimestampCheck::NoTimestamp,
|
||||
VisitTimeStampCheck::NewTimestamp,
|
||||
);
|
||||
assert_logged_in(resp, None).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_cookie_not_updated_on_login_deadline() {
|
||||
let srv = create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
|
||||
let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_no_login_cookie(&mut resp);
|
||||
assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_identity_cookie_updated_on_visit_deadline() {
|
||||
let srv = create_identity_server(|c| {
|
||||
c.visit_deadline(Duration::days(90))
|
||||
.login_deadline(Duration::days(90))
|
||||
})
|
||||
.await;
|
||||
let timestamp = SystemTime::now() - Duration::days(1);
|
||||
let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
|
||||
let mut resp = test::call_service(
|
||||
&srv,
|
||||
TestRequest::with_uri("/")
|
||||
.cookie(cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_login_cookie(
|
||||
&mut resp,
|
||||
COOKIE_LOGIN,
|
||||
LoginTimestampCheck::OldTimestamp(timestamp),
|
||||
VisitTimeStampCheck::NewTimestamp,
|
||||
);
|
||||
assert_logged_in(resp, Some(COOKIE_LOGIN)).await;
|
||||
}
|
||||
}
|
101
actix-identity/src/identity.rs
Normal file
101
actix-identity/src/identity.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use actix_web::{
|
||||
dev::{Extensions, Payload},
|
||||
Error, FromRequest, HttpRequest,
|
||||
};
|
||||
use futures_util::future::{ready, Ready};
|
||||
|
||||
pub(crate) struct IdentityItem {
|
||||
pub(crate) id: Option<String>,
|
||||
pub(crate) changed: bool,
|
||||
}
|
||||
|
||||
/// The extractor type to obtain your identity from a request.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::*;
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index(id: Identity) -> impl Responder {
|
||||
/// // access request identity
|
||||
/// if let Some(id) = id.identity() {
|
||||
/// format!("Welcome! {}", id)
|
||||
/// } else {
|
||||
/// "Welcome Anonymous!".to_owned()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[post("/login")]
|
||||
/// async fn login(id: Identity) -> impl Responder {
|
||||
/// // remember identity
|
||||
/// id.remember("User1".to_owned());
|
||||
///
|
||||
/// HttpResponse::Ok()
|
||||
/// }
|
||||
///
|
||||
/// #[post("/logout")]
|
||||
/// async fn logout(id: Identity) -> impl Responder {
|
||||
/// // remove identity
|
||||
/// id.forget();
|
||||
///
|
||||
/// HttpResponse::Ok()
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Identity(HttpRequest);
|
||||
|
||||
impl Identity {
|
||||
/// Return the claimed identity of the user associated request or `None` if no identity can be
|
||||
/// found associated with the request.
|
||||
pub fn identity(&self) -> Option<String> {
|
||||
Identity::get_identity(&self.0.extensions())
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_identity(extensions: &Extensions) -> Option<String> {
|
||||
let id = extensions.get::<IdentityItem>()?;
|
||||
id.id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extractor implementation for Identity type.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_web::*;
|
||||
/// use actix_identity::Identity;
|
||||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index(id: Identity) -> impl Responder {
|
||||
/// // access request identity
|
||||
/// if let Some(id) = id.identity() {
|
||||
/// format!("Welcome! {}", id)
|
||||
/// } else {
|
||||
/// "Welcome Anonymous!".to_owned()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
impl FromRequest for Identity {
|
||||
type Config = ();
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Identity, Error>>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ready(Ok(Identity(req.clone())))
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
168
actix-identity/src/middleware.rs
Normal file
168
actix-identity/src/middleware.rs
Normal file
@ -0,0 +1,168 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use actix_web::{
|
||||
dev::{Service, ServiceRequest, ServiceResponse, Transform},
|
||||
Error, HttpMessage, Result,
|
||||
};
|
||||
use futures_util::future::{ready, FutureExt, LocalBoxFuture, Ready};
|
||||
|
||||
use crate::{identity::IdentityItem, IdentityPolicy};
|
||||
|
||||
/// Request identity middleware
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::App;
|
||||
/// use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
///
|
||||
/// // create cookie identity backend
|
||||
/// let policy = CookieIdentityPolicy::new(&[0; 32])
|
||||
/// .name("auth-cookie")
|
||||
/// .secure(false);
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// // wrap policy into identity middleware
|
||||
/// .wrap(IdentityService::new(policy));
|
||||
/// ```
|
||||
pub struct IdentityService<T> {
|
||||
backend: Rc<T>,
|
||||
}
|
||||
|
||||
impl<T> IdentityService<T> {
|
||||
/// Create new identity service with specified backend.
|
||||
pub fn new(backend: T) -> Self {
|
||||
IdentityService {
|
||||
backend: Rc::new(backend),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T, B> Transform<S, ServiceRequest> for IdentityService<T>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
T: IdentityPolicy,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Transform = IdentityServiceMiddleware<S, T>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(IdentityServiceMiddleware {
|
||||
backend: self.backend.clone(),
|
||||
service: Rc::new(service),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IdentityServiceMiddleware<S, T> {
|
||||
pub(crate) service: Rc<S>,
|
||||
pub(crate) backend: Rc<T>,
|
||||
}
|
||||
|
||||
impl<S, T> Clone for IdentityServiceMiddleware<S, T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
backend: Rc::clone(&self.backend),
|
||||
service: Rc::clone(&self.service),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T, B> Service<ServiceRequest> for IdentityServiceMiddleware<S, T>
|
||||
where
|
||||
B: 'static,
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||
S::Future: 'static,
|
||||
T: IdentityPolicy,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::forward_ready!(service);
|
||||
|
||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||
let srv = Rc::clone(&self.service);
|
||||
let backend = Rc::clone(&self.backend);
|
||||
let fut = self.backend.from_request(&mut req);
|
||||
|
||||
async move {
|
||||
match fut.await {
|
||||
Ok(id) => {
|
||||
req.extensions_mut()
|
||||
.insert(IdentityItem { id, changed: false });
|
||||
|
||||
let mut res = srv.call(req).await?;
|
||||
let id = res.request().extensions_mut().remove::<IdentityItem>();
|
||||
|
||||
if let Some(id) = id {
|
||||
match backend.to_response(id.id, id.changed, &mut res).await {
|
||||
Ok(_) => Ok(res),
|
||||
Err(e) => Ok(res.error_response(e)),
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
Err(err) => Ok(req.error_response(err)),
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use actix_service::into_service;
|
||||
use actix_web::{dev, error, test, Error, Result};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_borrowed_mut_error() {
|
||||
use futures_util::future::{lazy, ok, Ready};
|
||||
|
||||
struct Ident;
|
||||
impl IdentityPolicy for Ident {
|
||||
type Future = Ready<Result<Option<String>, Error>>;
|
||||
type ResponseFuture = Ready<Result<(), Error>>;
|
||||
|
||||
fn from_request(&self, _: &mut dev::ServiceRequest) -> Self::Future {
|
||||
ok(Some("test".to_string()))
|
||||
}
|
||||
|
||||
fn to_response<B>(
|
||||
&self,
|
||||
_: Option<String>,
|
||||
_: bool,
|
||||
_: &mut dev::ServiceResponse<B>,
|
||||
) -> Self::ResponseFuture {
|
||||
ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let srv = crate::middleware::IdentityServiceMiddleware {
|
||||
backend: Rc::new(Ident),
|
||||
service: Rc::new(into_service(|_: dev::ServiceRequest| async move {
|
||||
actix_rt::time::sleep(Duration::from_secs(100)).await;
|
||||
Err::<dev::ServiceResponse, _>(error::ErrorBadRequest("error"))
|
||||
})),
|
||||
};
|
||||
|
||||
let srv2 = srv.clone();
|
||||
let req = test::TestRequest::default().to_srv_request();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
let _ = srv2.call(req).await;
|
||||
});
|
||||
|
||||
actix_rt::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
let _ = lazy(|cx| srv.poll_ready(cx)).await;
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
|
||||
## 0.9.2 - 2020-03-21
|
||||
* Implement `std::error::Error` for `Error` [#135]
|
||||
* Allow the removal of Max-Age for session-only cookies. [#161]
|
||||
* Allow the removal of `Max-Age` for session-only cookies. [#161]
|
||||
|
||||
[#135]: https://github.com/actix/actix-extras/pull/135
|
||||
[#161]: https://github.com/actix/actix-extras/pull/161
|
||||
|
@ -8,7 +8,6 @@
|
||||
## 0.4.1 - 2020-03-21
|
||||
* `Session::set_session` takes a `IntoIterator` instead of `Iterator`. [#105]
|
||||
* Fix calls to `session.purge()` from paths other than the one specified in the cookie. [#129]
|
||||
* Minimum supported Rust version (MSRV) is now 1.46.0.
|
||||
|
||||
[#105]: https://github.com/actix/actix-extras/pull/105
|
||||
[#129]: https://github.com/actix/actix-extras/pull/129
|
||||
|
@ -7,7 +7,6 @@
|
||||
|
||||
## 0.5.1 - 2020-03-21
|
||||
* Correct error handling when extracting auth details from request. [#128]
|
||||
* Minimum supported Rust version (MSRV) is now 1.46.0.
|
||||
|
||||
[#128]: https://github.com/actix/actix-extras/pull/128
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user