asjeeves_csrf/
form_authenticity_token.rs1use std::{borrow::Cow, convert::Infallible, fmt};
12
13use axum::{
14 extract::{FromRequestParts, OptionalFromRequestParts},
15 http::HeaderValue,
16};
17use axum_extra::extract::CookieJar;
18use base64ct::{Base64Url, Encoding};
19use cookie::{Cookie, SameSite};
20
21use rand_core::RngCore;
22
23use tracing::instrument;
24
25pub const COOKIE_NAME: &str = "csrf_token";
26
27const TOKEN_LEN: usize = 32;
29
30#[derive(Clone, Debug, PartialEq)]
31pub struct FormAuthenticityToken(Box<str>);
32
33impl FormAuthenticityToken {
34 #[instrument]
48 pub fn generate<R>(rng: &mut R) -> Self
49 where
50 R: fmt::Debug + RngCore,
51 {
52 let mut token_bytes = [0u8; TOKEN_LEN];
54
55 rng.fill_bytes(&mut token_bytes);
56
57 let token = Base64Url::encode_string(&token_bytes);
58
59 Self(token.into_boxed_str())
60 }
61
62 pub fn csrf_cookie<'a>(&self) -> Cookie<'a> {
65 Cookie::build((COOKIE_NAME, self.to_string()))
66 .http_only(true)
67 .partitioned(true)
68 .same_site(SameSite::Strict)
69 .secure(true)
70 .path("/")
71 .build()
72 }
73
74 pub fn as_str(&self) -> &str {
75 self.0.as_ref()
76 }
77}
78
79impl AsRef<str> for FormAuthenticityToken {
80 fn as_ref(&self) -> &str {
81 self.0.as_ref()
82 }
83}
84
85impl fmt::Display for FormAuthenticityToken {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "{}", self.0.as_ref())
88 }
89}
90
91impl<'a> From<&Cookie<'a>> for FormAuthenticityToken {
92 fn from(c: &Cookie<'a>) -> Self {
93 let s = String::from(c.value());
94
95 Self(s.into_boxed_str())
96 }
97}
98
99impl From<HeaderValue> for FormAuthenticityToken {
100 fn from(value: HeaderValue) -> Self {
101 let bytes: &[u8] = value.as_bytes();
102
103 let s: Cow<'_, str> = String::from_utf8_lossy(bytes);
104
105 Self(s.into())
106 }
107}
108
109impl<S> OptionalFromRequestParts<S> for FormAuthenticityToken
110where
111 S: Send + Sync,
112{
113 type Rejection = Infallible;
114
115 async fn from_request_parts(
116 parts: &mut axum::http::request::Parts,
117 state: &S,
118 ) -> Result<Option<Self>, Self::Rejection> {
119 let jar: CookieJar = CookieJar::from_request_parts(parts, state).await?;
120 let fat: Option<Self> = jar.get(COOKIE_NAME).map(Self::from);
121
122 Ok(fat)
123 }
124}
125
126#[cfg(test)]
127pub mod test {
128 use super::*;
129 use rand_chacha::ChaCha20Rng;
130 use rand_core::SeedableRng;
131
132 pub const FAT_ONE: &'static str = "mjdEUEVgY57GcLehfUkrJz4HewqWvvWLp3YHeeVEVG4=";
133 pub const FAT_TWO: &'static str = "AA7-yHxXSewRV5EuDhcfYN6eU0E0iBmi3pnxQMWaQkw=";
134
135 #[test]
136 fn it_should_create_a_token_and_cookie() {
137 let mut rng = ChaCha20Rng::seed_from_u64(1);
138
139 let fat_one = FormAuthenticityToken::generate(&mut rng);
140 let fat_two = FormAuthenticityToken::generate(&mut rng);
141
142 let ec_one = format!(
143 "{}={}; HttpOnly; SameSite=Strict; Partitioned; Secure; Path=/",
144 COOKIE_NAME, FAT_ONE
145 );
146 let ec_two = format!(
147 "{}={}; HttpOnly; SameSite=Strict; Partitioned; Secure; Path=/",
148 COOKIE_NAME, FAT_TWO
149 );
150
151 assert_eq!(FAT_ONE, fat_one.as_str());
152 assert_eq!(FAT_ONE, fat_one.as_ref());
153 assert_eq!(ec_one, fat_one.csrf_cookie().to_string());
154
155 assert_eq!(FAT_TWO, fat_two.as_str());
156 assert_eq!(FAT_TWO, fat_two.as_ref());
157 assert_eq!(ec_two, fat_two.csrf_cookie().to_string());
158 }
159}