1use subtle::ConstantTimeEq;
6use topcoat::{
7 context::{Cx, try_request_context},
8 cookie::{Cookie, CookieJarCell, Cookies, cookies},
9};
10
11pub const COOKIE_NAME: &str = "__Host-tablo_csrf";
13pub const FIELD_NAME: &str = "csrf_token";
14
15pub fn ensure_token(cx: &Cx) -> String {
17 if try_request_context::<CookieJarCell>(cx).is_none() {
18 return String::new();
19 }
20 let jar = cookies(cx);
21 if let Some(cookie) = jar.get(COOKIE_NAME) {
22 let value = cookie.value().to_string();
23 if is_valid_token(&value) {
24 return value;
25 }
26 }
27 let token = uuid::Uuid::new_v4().to_string();
28 let cookie = Cookie::build((COOKIE_NAME, token.clone()))
29 .path("/")
30 .http_only(true)
31 .secure(true)
32 .same_site(topcoat::cookie::SameSite::Lax)
33 .build();
34 jar.add(cookie);
35 token
36}
37
38pub fn current_token(cx: &Cx) -> String {
40 if try_request_context::<CookieJarCell>(cx).is_none() {
41 return String::new();
42 }
43 cookies(cx)
44 .get(COOKIE_NAME)
45 .map(|c| c.value().to_string())
46 .filter(|v| is_valid_token(v))
47 .unwrap_or_default()
48}
49
50pub fn field<'a>(cx: &'a Cx, token: &str) -> topcoat::view::BoxView<'a> {
52 use topcoat::view::ViewExt;
53
54 let token = token.to_string();
55 topcoat::view::view! { cx => <input type="hidden" name=(FIELD_NAME) value=(token)> }.boxed()
56}
57
58pub fn verify(
61 cx: &Cx,
62 values: &std::collections::HashMap<String, String>,
63) -> Result<(), topcoat::Error> {
64 let cookie_ok = try_request_context::<CookieJarCell>(cx)
65 .map(|_| cookies(cx).get(COOKIE_NAME).map(|c| c.value().to_string()))
66 .unwrap_or(None);
67 let Some(expected) = cookie_ok else {
68 return Err(topcoat::router::error::forbidden().into());
69 };
70 let Some(submitted) = values.get(FIELD_NAME) else {
71 return Err(topcoat::router::error::forbidden().into());
72 };
73 if !is_valid_token(&expected) || submitted.as_bytes().ct_ne(expected.as_bytes()).into() {
74 return Err(topcoat::router::error::forbidden().into());
75 }
76 Ok(())
77}
78
79fn is_valid_token(value: &str) -> bool {
80 value.parse::<uuid::Uuid>().is_ok()
81}
82
83#[cfg(test)]
84mod tests;