authzen-session 0.1.0-alpha.0

Utilities for integrating session management with different web frameworks and support for different session storage backends.
Documentation
use chrono::{Duration, NaiveDateTime};
use data_encoding::BASE64;
use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS};
use ring::hmac::{sign, Key};
use serde::de::{Deserializer, Error};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use uuid::Uuid;

const PERCENT_ENCODING_ASCII_SET: &AsciiSet = &CONTROLS.add(b':').add(b'=');

#[derive(Clone, Debug)]
pub struct CookieValue {
    pub id: Uuid,
    pub signature: String,
}

impl CookieValue {
    pub(crate) fn new(key: &Key) -> Result<Self, anyhow::Error> {
        let id = Uuid::new_v4();
        let signature = BASE64.encode(sign(key, id.as_bytes()).as_ref());
        Ok(Self { id, signature })
    }

    pub(crate) fn encode(&self) -> String {
        serde_plain::to_string(&self).unwrap()
    }
}

impl Serialize for CookieValue {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let formatted = format!(
            "s:{}.{}",
            self.id.as_simple().encode_lower(&mut Uuid::encode_buffer()),
            self.signature
        );
        let encoded = utf8_percent_encode(&formatted, PERCENT_ENCODING_ASCII_SET).to_string();
        serializer.collect_str(&encoded)
    }
}

impl<'de> Deserialize<'de> for CookieValue {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let encoded_cookie_value = String::deserialize(deserializer)?;
        let cookie_value = percent_decode(encoded_cookie_value.as_bytes())
            .decode_utf8()
            .map_err(|_| D::Error::custom("invalid cookie value"))?;
        if &cookie_value[0..2] != "s:" {
            return Err(D::Error::custom("invalid cookie value"));
        }
        let mut sequence: Vec<_> = cookie_value[2..].split('.').map(|item| item.to_owned()).collect();
        if sequence.len() != 2 {
            return Err(D::Error::custom("invalid cookie value"));
        }
        let signature = sequence.pop().unwrap();
        let id = Uuid::parse_str(&sequence.pop().unwrap()).map_err(|_| D::Error::custom("invalid cookie value"))?;

        Ok(CookieValue { signature, id })
    }
}

#[derive(Clone, Debug)]
pub struct CookieConfig<'a, T: 'a + Clone> {
    pub value: &'a T,
    pub http_only: bool,
    pub secure: bool,
    pub same_site: SameSite,
    pub domain: Option<Cow<'a, str>>,
    pub path: Option<Cow<'a, str>>,
    pub max_age: Option<Duration>,
    pub expires: Option<NaiveDateTime>,
}

impl<'a, T: 'a + Clone + Deserialize<'a> + Serialize> CookieConfig<'a, T> {
    pub fn new(value: &'a T) -> Self {
        Self {
            http_only: true,
            secure: true,
            same_site: SameSite::Lax,
            domain: None,
            path: None,
            max_age: None,
            expires: None,
            value,
        }
    }
    pub fn http_only(mut self, http_only: bool) -> Self {
        self.http_only = http_only;
        self
    }
    pub fn secure(mut self, secure: bool) -> Self {
        self.secure = secure;
        self
    }
    pub fn same_site(mut self, same_site: SameSite) -> Self {
        self.same_site = same_site;
        self
    }
    pub fn max_age(mut self, max_age: impl Into<Option<Duration>>) -> Self {
        self.max_age = max_age.into();
        self
    }
    pub fn expires(mut self, expires: impl Into<Option<NaiveDateTime>>) -> Self {
        self.expires = expires.into();
        self
    }
    pub fn domain<S: Into<Cow<'a, str>>>(mut self, domain: impl Into<Option<S>>) -> Self {
        self.domain = domain.into().map(Into::into);
        self
    }
    pub fn path<S: Into<Cow<'a, str>>>(mut self, path: impl Into<Option<S>>) -> Self {
        self.path = path.into().map(Into::into);
        self
    }
}

#[derive(Clone, Debug)]
pub enum SameSite {
    Strict,
    Lax,
    None,
}

impl std::fmt::Display for SameSite {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self {
            Self::Strict => write!(f, "Strict"),
            Self::Lax => write!(f, "Lax"),
            Self::None => write!(f, "None"),
        }
    }
}