diff options
Diffstat (limited to 'src/security.rs')
-rw-r--r-- | src/security.rs | 65 |
1 files changed, 65 insertions, 0 deletions
diff --git a/src/security.rs b/src/security.rs new file mode 100644 index 0000000..fe9c26b --- /dev/null +++ b/src/security.rs @@ -0,0 +1,65 @@ +//! [`CsrfToken`], [`Key`] and functions to encode & decode expiring claims. + +use rand::{Rng, distributions::Alphanumeric}; +use ::cookie::Cookie; +use time::{Duration, OffsetDateTime}; + +pub use crate::signed::Key; + +use crate::{Error, Request, Response}; + +/// A cookie-based CSRF token to be used with [`crate::Request::into_form_csrf`]. +pub struct CsrfToken { + token: String, + from_client: bool, +} + +impl CsrfToken { + /// Retrieves the CSRF token from a `csrf` cookie or generates + /// a new token and stores it as a cookie if it doesn't exist. + pub fn from_request(request: &mut Request, response: &mut Response) -> Self { + if let Some(cookie) = request.cookies().get("csrf") { + return CsrfToken{token: cookie.value().to_string(), from_client: true} + } + let val: String = rand::thread_rng().sample_iter(Alphanumeric).take(16).collect(); + let mut c = Cookie::new("csrf", val.clone()); + c.set_secure(Some(true)); + c.set_max_age(Some(Duration::hours(1))); + response.set_cookie(c); + CsrfToken{token: val, from_client: false} + } + + /// Wraps the token in a hidden HTML input. + pub fn html_input(&self) -> String { + format!("<input name=csrf type=hidden value=\"{}\">", self.token) + } + + pub(crate) fn matches(&self, str: String) -> Result<(), Error> { + if !self.from_client { + return Err(Error::bad_request("expected csrf cookie".to_string())) + } + if self.token != str { + return Err(Error::bad_request("csrf parameter doesn't match csrf cookie".to_string())) + } + Ok(()) + } +} + +/// Join a string and an expiry date together into a string. +pub fn encode_expiring_claim(claim: &str, expiry_date: OffsetDateTime) -> String { + format!("{}:{}", claim, expiry_date.unix_timestamp()) +} + +/// Extract the string, failing if the expiry date is in the past. +pub fn decode_expiring_claim(value: String) -> Result<String,&'static str> { + let mut parts = value.splitn(2, ':'); + let claim = parts.next().ok_or("expected colon")?; + let expiry_date = parts.next().ok_or("expected colon")?; + let expiry_date: i64 = expiry_date.parse().map_err(|_| "failed to parse timestamp")?; + + if expiry_date > OffsetDateTime::now_utc().unix_timestamp() { + Ok(claim.to_string()) + } else { + Err("token is expired") + } +}
\ No newline at end of file |