rust 64 lines · 8 steps

Building a replay-guard extractor in Axum

A custom FromRequestParts extractor rejects stale or replayed requests using a timestamp and a shared per-client seen map.

Explained by highlit
1use axum::{
2 extract::FromRequestParts,
3 http::{request::Parts, HeaderMap, StatusCode},
4};
5use std::{
6 collections::HashMap,
7 sync::{Arc, Mutex},
8 time::{Duration, SystemTime, UNIX_EPOCH},
9};
10 
11#[derive(Clone, Default)]
12pub struct ReplayGuard {
13 last_seen: Arc<Mutex<HashMap<String, u64>>>,
14}
15 
16pub struct FreshRequest {
17 pub client_id: String,
18 pub timestamp_ms: u64,
19}
20 
21const MAX_SKEW: Duration = Duration::from_secs(30);
22 
23impl<S> FromRequestParts<S> for FreshRequest
24where
25 ReplayGuard: FromRef<S>,
26 S: Send + Sync,
27{
28 type Rejection = (StatusCode, &'static str);
29 
30 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
31 let guard = ReplayGuard::from_ref(state);
32 let client_id = header(&parts.headers, "x-client-id")?.to_owned();
33 let timestamp_ms: u64 = header(&parts.headers, "x-request-timestamp")?
34 .parse()
35 .map_err(|_| (StatusCode::BAD_REQUEST, "malformed X-Request-Timestamp"))?;
36 
37 let now = SystemTime::now()
38 .duration_since(UNIX_EPOCH)
39 .unwrap()
40 .as_millis() as u64;
41 if timestamp_ms.abs_diff(now) > MAX_SKEW.as_millis() as u64 {
42 return Err((StatusCode::UNAUTHORIZED, "timestamp outside allowed skew"));
43 }
44 
45 let mut seen = guard.last_seen.lock().unwrap();
46 match seen.get(&client_id) {
47 Some(&prev) if timestamp_ms <= prev => {
48 return Err((StatusCode::CONFLICT, "stale or replayed X-Request-Timestamp"));
49 }
50 _ => {
51 seen.insert(client_id.clone(), timestamp_ms);
52 }
53 }
54 
55 Ok(FreshRequest { client_id, timestamp_ms })
56 }
57}
58 
59fn header<'a>(headers: &'a HeaderMap, name: &'static str) -> Result<&'a str, (StatusCode, &'static str)> {
60 headers
61 .get(name)
62 .and_then(|v| v.to_str().ok())
63 .ok_or((StatusCode::BAD_REQUEST, "missing required header"))
64}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Implementing FromRequestParts lets you fold authentication and validation into a handler argument that runs before your logic.
  2. 2Bounding a timestamp by a maximum clock skew and rejecting non-increasing values together defend against replay attacks.
  3. 3Sharing mutable state across requests via Arc<Mutex<..>> and FromRef keeps per-client bookkeeping accessible inside extractors.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a replay-guard extractor in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code