aboutsummaryrefslogtreecommitdiff
path: root/src/security.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/security.rs')
-rw-r--r--src/security.rs65
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