//! [`CsrfToken`], [`Key`] and functions to encode & decode expiring claims. use rand::{Rng, distributions::Alphanumeric}; use ::cookie::Cookie; use time::{Duration, OffsetDateTime}; use thiserror::Error; pub use crate::signed::Key; use crate::{request::Parts, response::Response}; /// A cookie-based CSRF token to be used with [`crate::request::Body::into_form_csrf`]. pub struct CsrfToken { token: String, from_client: bool, } #[derive(Error, Debug)] pub enum CsrfError { #[error("expected csrf cookie")] NoCookie, #[error("csrf parameter doesn't match csrf cookie")] Mismatch, } 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_parts(request: &mut Parts, 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!("", self.token) } pub(crate) fn matches(&self, str: String) -> Result<(), CsrfError> { if !self.from_client { return Err(CsrfError::NoCookie) } if self.token != str { return Err(CsrfError::Mismatch) } 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 { 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") } }