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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Rotating refresh tokens on every use lets you detect theft: a replayed old token signals compromise.
- 2Tracking a family_id across rotations means one leaked token can revoke the whole lineage at once.
- 3A single write lock over the store makes the check-then-mutate sequence atomic against concurrent refreshes.
Related explainers
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config';
How a JWT strategy authenticates in NestJS
authentication
jwt
passport
Intermediate
7 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
rust
use std::cmp::Ordering; pub struct PrefixIndex { entries: Vec<String>,
Prefix search with binary partitioning in Rust
binary-search
sorting
case-insensitive
Intermediate
7 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 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/refresh-token-rotation-in-axum-explained-rust-8d36/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.