rust 58 lines · 8 steps

Verifying Stripe webhook signatures in Axum

An Axum handler that authenticates Stripe webhooks with an HMAC-SHA256 signature and a timestamp freshness check.

Explained by highlit
1use axum::{
2 body::Bytes,
3 extract::State,
4 http::{HeaderMap, StatusCode},
5 response::IntoResponse,
6};
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9use std::time::{SystemTime, UNIX_EPOCH};
10use subtle::ConstantTimeEq;
11 
12type HmacSha256 = Hmac<Sha256>;
13 
14#[derive(Clone)]
15pub struct WebhookState {
16 pub signing_secret: String,
17}
18 
19pub async fn handle_stripe_webhook(
20 State(state): State<WebhookState>,
21 headers: HeaderMap,
22 body: Bytes,
23) -> impl IntoResponse {
24 let sig_header = match headers.get("Stripe-Signature").and_then(|v| v.to_str().ok()) {
25 Some(h) => h,
26 None => return (StatusCode::BAD_REQUEST, "missing signature").into_response(),
27 };
28 
29 let mut timestamp = None;
30 let mut expected = None;
31 for part in sig_header.split(',') {
32 match part.split_once('=') {
33 Some(("t", v)) => timestamp = v.parse::<i64>().ok(),
34 Some(("v1", v)) => expected = hex::decode(v).ok(),
35 _ => {}
36 }
37 }
38 
39 let (Some(ts), Some(expected)) = (timestamp, expected) else {
40 return (StatusCode::BAD_REQUEST, "malformed signature").into_response();
41 };
42 
43 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
44 if (now - ts).abs() > 300 {
45 return (StatusCode::BAD_REQUEST, "timestamp too old").into_response();
46 }
47 
48 let signed_payload = format!("{ts}.{}", String::from_utf8_lossy(&body));
49 let mut mac = HmacSha256::new_from_slice(state.signing_secret.as_bytes()).unwrap();
50 mac.update(signed_payload.as_bytes());
51 let computed = mac.finalize().into_bytes();
52 
53 if computed.ct_eq(&expected).unwrap_u8() != 1 {
54 return (StatusCode::UNAUTHORIZED, "invalid signature").into_response();
55 }
56 
57 StatusCode::OK.into_response()
58}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Webhook authenticity comes from recomputing an HMAC over the exact signed payload with a shared secret.
  2. 2Comparing signatures in constant time prevents timing attacks that could leak the correct value.
  3. 3A timestamp tolerance window blocks replay of old, previously-valid requests.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Verifying Stripe webhook signatures in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code