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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Safe HTTP methods can skip CSRF checks because they aren't supposed to mutate state.
- 2Consuming a request body forces you to rebuild the request before passing it downstream.
- 3Comparing secrets in constant time avoids leaking information through timing differences.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
Intermediate
7 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-csrf-verification-middleware-in-axum-explained-rust-24f5/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.