rust
70 lines · 9 steps
Signed download links as an Axum extractor
An HMAC-signed, time-limited URL scheme enforced automatically through a custom Axum extractor.
Explained by
highlit
1use axum::{
2 extract::{FromRequestParts, Path, Query},
3 http::{request::Parts, StatusCode},
4 response::{IntoResponse, Redirect},
5};
6use hmac::{Hmac, Mac};
7use serde::Deserialize;
8use sha2::Sha256;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11type HmacSha256 = Hmac<Sha256>;
12const SECRET: &[u8] = b"change-me-in-config";
13
14fn sign(object: &str, expires: u64) -> String {
15 let mut mac = HmacSha256::new_from_slice(SECRET).expect("valid key length");
16 mac.update(object.as_bytes());
17 mac.update(&expires.to_be_bytes());
18 hex::encode(mac.finalize().into_bytes())
19}
20
21pub async fn create_download_url(Path(object): Path<String>) -> impl IntoResponse {
22 let expires = SystemTime::now()
23 .duration_since(UNIX_EPOCH)
24 .unwrap()
25 .as_secs()
26 + 300;
27 let sig = sign(&object, expires);
28 let url = format!("/files/{object}?expires={expires}&sig={sig}");
29 (StatusCode::CREATED, url)
30}
31
32#[derive(Deserialize)]
33struct SignedParams {
34 expires: u64,
35 sig: String,
36}
37
38pub struct VerifiedDownload(pub String);
39
40impl<S: Send + Sync> FromRequestParts<S> for VerifiedDownload {
41 type Rejection = (StatusCode, &'static str);
42
43 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
44 let Path(object) = Path::<String>::from_request_parts(parts, state)
45 .await
46 .map_err(|_| (StatusCode::BAD_REQUEST, "missing object"))?;
47 let Query(params) = Query::<SignedParams>::from_request_parts(parts, state)
48 .await
49 .map_err(|_| (StatusCode::BAD_REQUEST, "missing signature"))?;
50
51 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
52 if params.expires < now {
53 return Err((StatusCode::FORBIDDEN, "link expired"));
54 }
55
56 let mut mac = HmacSha256::new_from_slice(SECRET).unwrap();
57 mac.update(object.as_bytes());
58 mac.update(¶ms.expires.to_be_bytes());
59 let expected = hex::decode(¶ms.sig)
60 .map_err(|_| (StatusCode::FORBIDDEN, "malformed signature"))?;
61 mac.verify_slice(&expected)
62 .map_err(|_| (StatusCode::FORBIDDEN, "invalid signature"))?;
63
64 Ok(VerifiedDownload(object))
65 }
66}
67
68pub async fn download(VerifiedDownload(object): VerifiedDownload) -> impl IntoResponse {
69 Redirect::temporary(&format!("https://cdn.internal/blobs/{object}"))
70}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1HMAC over the object name plus an expiry timestamp lets a server hand out tamper-proof links without storing per-link state.
- 2Modeling verification as a FromRequestParts extractor pushes the security check into the type system so handlers only run on valid requests.
- 3Always compare signatures with a constant-time verifier and reject expired timestamps before trusting any signed parameter.
Related explainers
rust
use axum::{ body::Body, extract::State, http::{header, StatusCode},
Streaming a DB migration with Axum
streaming
keyset-pagination
backpressure
Advanced
8 steps
go
package middleware import ( "crypto/hmac"
Verifying signed URLs with Gin middleware
hmac
middleware
authentication
Intermediate
8 steps
javascript
import { createContext, useContext, useReducer, useCallback, useEffect } from 'react'; const AuthContext = createContext(null);
Building an auth context in React
context
usereducer
authentication
Intermediate
8 steps
rust
use axum::{ extract::{FromRef, FromRequestParts}, http::{header, request::Parts, StatusCode}, RequestPartsExt,
How a JWT extractor works in Axum
jwt
authentication
extractors
Intermediate
8 steps
rust
use std::collections::HashMap; #[derive(Clone, Copy, PartialEq)] enum Color {
Detecting cycles with three-color DFS in Rust
graph-algorithms
cycle-detection
depth-first-search
Intermediate
9 steps
rust
#[derive(Deserialize)] pub struct CreateArticle { title: String, body: String,
Building a create endpoint in Axum
extractors
json-deserialization
sqlx
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/signed-download-links-as-an-axum-extractor-explained-rust-cb95/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.