rust 62 lines · 10 steps

A CSRF verification middleware in Axum

An Axum middleware that checks form submissions carry a CSRF token matching a signed cookie before letting the request through.

Explained by highlit
1use axum::{
2 body::Body,
3 extract::Request,
4 http::{header, StatusCode},
5 middleware::Next,
6 response::{IntoResponse, Response},
7};
8use axum_extra::extract::cookie::{CookieJar, SignedCookieJar};
9use hyper::header::CONTENT_TYPE;
10 
11const CSRF_COOKIE: &str = "csrf_token";
12const CSRF_FIELD: &str = "_csrf";
13 
14pub async fn verify_csrf(jar: SignedCookieJar, request: Request, next: Next) -> Response {
15 if matches!(request.method(), &http::Method::GET | &http::Method::HEAD | &http::Method::OPTIONS) {
16 return next.run(request).await;
17 }
18 
19 let expected = match jar.get(CSRF_COOKIE) {
20 Some(cookie) => cookie.value().to_owned(),
21 None => return forbidden("missing csrf cookie"),
22 };
23 
24 let is_form = request
25 .headers()
26 .get(CONTENT_TYPE)
27 .and_then(|v| v.to_str().ok())
28 .is_some_and(|ct| ct.starts_with("application/x-www-form-urlencoded"));
29 
30 if !is_form {
31 return forbidden("unsupported content type for csrf check");
32 }
33 
34 let (parts, body) = request.into_parts();
35 let bytes = match axum::body::to_bytes(body, 64 * 1024).await {
36 Ok(bytes) => bytes,
37 Err(_) => return forbidden("could not read request body"),
38 };
39 
40 let submitted = form_urlencoded::parse(&bytes)
41 .find(|(key, _)| key == CSRF_FIELD)
42 .map(|(_, value)| value.into_owned());
43 
44 match submitted {
45 Some(token) if constant_time_eq(token.as_bytes(), expected.as_bytes()) => {
46 let rebuilt = Request::from_parts(parts, Body::from(bytes));
47 next.run(rebuilt).await
48 }
49 _ => forbidden("invalid or stale csrf token"),
50 }
51}
52 
53fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
54 if a.len() != b.len() {
55 return false;
56 }
57 a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
58}
59 
60fn forbidden(reason: &'static str) -> Response {
61 (StatusCode::FORBIDDEN, reason).into_response()
62}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Safe HTTP methods can skip CSRF checks because they aren't supposed to mutate state.
  2. 2Consuming a request body forces you to rebuild the request before passing it downstream.
  3. 3Comparing secrets in constant time avoids leaking information through timing differences.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A CSRF verification middleware in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code