aboutsummaryrefslogtreecommitdiff
path: root/src/security.rs
blob: 5247d9ee52bc438014dca6eec7d75413c11aaa0b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//! [`CsrfToken`], [`Key`] and functions to encode & decode expiring claims.

use time::OffsetDateTime;
use thiserror::Error;

pub use crate::signed::Key;

/// A cookie-based CSRF token to be used with [`crate::request::SputnikBody::into_form_csrf`].
pub struct CsrfToken {
    pub(crate) token: String,
    pub(crate) 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 {
    /// 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<(), 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<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")
    }
}