1use base64ct::{Base64UrlUnpadded, Encoding};
9use chrono::{DateTime, Duration, Utc};
10use mas_data_model::Clock;
11use rand::{
12 Rng, RngExt,
13 distr::{Distribution as _, StandardUniform},
14};
15use serde::{Deserialize, Serialize};
16use serde_with::{TimestampSeconds, serde_as};
17use thiserror::Error;
18
19use crate::cookies::{CookieDecodeError, CookieJar};
20
21#[derive(Debug, Error)]
23pub enum CsrfError {
24 #[error("CSRF token mismatch")]
26 Mismatch,
27
28 #[error("Missing CSRF cookie")]
30 Missing,
31
32 #[error("could not decode CSRF cookie")]
34 DecodeCookie(#[from] CookieDecodeError),
35
36 #[error("CSRF token expired")]
38 Expired,
39
40 #[error("could not decode CSRF token")]
42 Decode(#[from] base64ct::Error),
43}
44
45#[serde_as]
47#[derive(Serialize, Deserialize, Debug)]
48pub struct CsrfToken {
49 #[serde_as(as = "TimestampSeconds<i64>")]
50 expiration: DateTime<Utc>,
51 token: [u8; 32],
52}
53
54impl CsrfToken {
55 fn new(token: [u8; 32], now: DateTime<Utc>, ttl: Duration) -> Self {
57 let expiration = now + ttl;
58 Self { expiration, token }
59 }
60
61 fn generate(now: DateTime<Utc>, mut rng: impl RngExt, ttl: Duration) -> Self {
63 let token = StandardUniform.sample(&mut rng);
64 Self::new(token, now, ttl)
65 }
66
67 fn refresh(self, now: DateTime<Utc>, ttl: Duration) -> Self {
69 Self::new(self.token, now, ttl)
70 }
71
72 #[must_use]
74 pub fn form_value(&self) -> String {
75 Base64UrlUnpadded::encode_string(&self.token[..])
76 }
77
78 pub fn verify_form_value(&self, form_value: &str) -> Result<(), CsrfError> {
84 let form_value = Base64UrlUnpadded::decode_vec(form_value)?;
85 if self.token[..] == form_value {
86 Ok(())
87 } else {
88 Err(CsrfError::Mismatch)
89 }
90 }
91
92 fn verify_expiration(self, now: DateTime<Utc>) -> Result<Self, CsrfError> {
93 if now < self.expiration {
94 Ok(self)
95 } else {
96 Err(CsrfError::Expired)
97 }
98 }
99}
100
101#[derive(Deserialize)]
103pub struct ProtectedForm<T> {
104 csrf: String,
105
106 #[serde(flatten)]
107 inner: T,
108}
109
110pub trait CsrfExt {
111 fn csrf_token<C, R>(self, clock: &C, rng: R) -> (CsrfToken, Self)
114 where
115 R: Rng,
116 C: Clock;
117
118 fn verify_form<C, T>(&self, clock: &C, form: ProtectedForm<T>) -> Result<T, CsrfError>
126 where
127 C: Clock;
128}
129
130impl CsrfExt for CookieJar {
131 fn csrf_token<C, R>(self, clock: &C, rng: R) -> (CsrfToken, Self)
132 where
133 R: Rng,
134 C: Clock,
135 {
136 let now = clock.now();
137 let maybe_token = match self.load::<CsrfToken>("csrf") {
138 Ok(Some(token)) => {
139 let token = token.verify_expiration(now);
140
141 token.ok()
143 }
144 Ok(None) => None,
145 Err(e) => {
146 tracing::warn!("Failed to decode CSRF cookie: {}", e);
147 None
148 }
149 };
150
151 let token = maybe_token.map_or_else(
152 || CsrfToken::generate(now, rng, Duration::try_hours(1).unwrap()),
153 |token| token.refresh(now, Duration::try_hours(1).unwrap()),
154 );
155
156 let jar = self.save("csrf", &token, false);
157 (token, jar)
158 }
159
160 fn verify_form<C, T>(&self, clock: &C, form: ProtectedForm<T>) -> Result<T, CsrfError>
161 where
162 C: Clock,
163 {
164 let token: CsrfToken = self.load("csrf")?.ok_or(CsrfError::Missing)?;
165 let token = token.verify_expiration(clock.now())?;
166 token.verify_form_value(&form.csrf)?;
167 Ok(form.inner)
168 }
169}