rust 71 lines · 9 steps

Refresh token rotation in Axum

An Axum handler that rotates refresh tokens and detects reuse by revoking an entire token family.

Explained by highlit
1use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
2use chrono::{Duration, Utc};
3use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, sync::Arc};
5use tokio::sync::RwLock;
6use uuid::Uuid;
7 
8#[derive(Clone)]
9struct StoredToken {
10 user_id: Uuid,
11 family_id: Uuid,
12 expires_at: chrono::DateTime<Utc>,
13 revoked: bool,
14}
15 
16#[derive(Clone, Default)]
17struct TokenStore(Arc<RwLock<HashMap<String, StoredToken>>>);
18 
19#[derive(Deserialize)]
20struct RefreshRequest {
21 refresh_token: String,
22}
23 
24#[derive(Serialize)]
25struct TokenPair {
26 access_token: String,
27 refresh_token: String,
28}
29 
30async fn refresh(
31 State(store): State<TokenStore>,
32 Json(req): Json<RefreshRequest>,
33) -> Result<Json<TokenPair>, StatusCode> {
34 let mut tokens = store.0.write().await;
35 
36 let current = tokens
37 .get(&req.refresh_token)
38 .cloned()
39 .ok_or(StatusCode::UNAUTHORIZED)?;
40 
41 if current.revoked {
42 for t in tokens.values_mut().filter(|t| t.family_id == current.family_id) {
43 t.revoked = true;
44 }
45 return Err(StatusCode::UNAUTHORIZED);
46 }
47 
48 if current.expires_at < Utc::now() {
49 return Err(StatusCode::UNAUTHORIZED);
50 }
51 
52 if let Some(old) = tokens.get_mut(&req.refresh_token) {
53 old.revoked = true;
54 }
55 
56 let new_refresh = Uuid::new_v4().to_string();
57 tokens.insert(
58 new_refresh.clone(),
59 StoredToken {
60 user_id: current.user_id,
61 family_id: current.family_id,
62 expires_at: Utc::now() + Duration::days(30),
63 revoked: false,
64 },
65 );
66 
67 Ok(Json(TokenPair {
68 access_token: issue_access_token(current.user_id),
69 refresh_token: new_refresh,
70 }))
71}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Rotating refresh tokens on every use lets you detect theft: a replayed old token signals compromise.
  2. 2Tracking a family_id across rotations means one leaked token can revoke the whole lineage at once.
  3. 3A single write lock over the store makes the check-then-mutate sequence atomic against concurrent refreshes.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Refresh token rotation in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code