use crate::request::ReqCtx;
use rand::Rng;
pub const COOKIE_NAME: &str = "adminx_csrf";
pub const FIELD_NAME: &str = "_csrf";
fn generate() -> String {
let bytes: [u8; 32] = rand::thread_rng().gen();
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn set_cookie_value(token: &str) -> String {
let mut v = format!("{COOKIE_NAME}={token}; HttpOnly; SameSite=Strict; Path=/");
if crate::auth::secure_cookie() {
v.push_str("; Secure");
}
v
}
pub fn ensure(ctx: &ReqCtx) -> (String, Option<String>) {
match ctx.csrf.as_deref().filter(|t| !t.is_empty()) {
Some(existing) => (existing.to_string(), None),
None => {
let token = generate();
let cookie = set_cookie_value(&token);
(token, Some(cookie))
}
}
}
fn ct_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
}
pub fn verify(ctx: &ReqCtx, submitted: Option<&str>) -> bool {
let cookie = match ctx.csrf.as_deref().filter(|t| !t.is_empty()) {
Some(c) => c,
None => return false,
};
match submitted.filter(|s| !s.is_empty()) {
Some(s) => ct_eq(cookie, s),
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx_with(token: Option<&str>) -> ReqCtx {
let ctx = ReqCtx::new();
match token {
Some(t) => ctx.with_csrf(t),
None => ctx,
}
}
#[test]
fn tokens_are_unique_and_hex() {
let (a, b) = (generate(), generate());
assert_ne!(a, b);
assert_eq!(a.len(), 64);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn ensure_mints_when_absent_and_reuses_when_present() {
let (token, cookie) = ensure(&ctx_with(None));
assert!(cookie.expect("should mint a cookie").contains(&token));
let (token, cookie) = ensure(&ctx_with(Some("existing")));
assert_eq!(token, "existing");
assert!(cookie.is_none(), "an existing token must be reused as-is");
}
#[test]
fn verify_requires_a_matching_pair() {
assert!(verify(&ctx_with(Some("abc")), Some("abc")));
assert!(!verify(&ctx_with(None), Some("abc")));
assert!(!verify(&ctx_with(Some("abc")), None));
assert!(!verify(&ctx_with(Some("abc")), Some("xyz")));
assert!(!verify(&ctx_with(Some("")), Some("")));
assert!(!verify(&ctx_with(Some("abc")), Some("")));
}
}