rust
71 lines · 9 steps
Custom auth extractors in Axum
Implementing FromRequestParts turns bearer-token validation into a reusable Axum handler argument.
Explained by
highlit
1use axum::{
2 async_trait,
3 extract::{FromRequestParts, State},
4 http::{header, request::Parts, StatusCode},
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde::Serialize;
9
10pub struct AuthRejection {
11 status: StatusCode,
12 message: &'static str,
13}
14
15impl IntoResponse for AuthRejection {
16 fn into_response(self) -> Response {
17 #[derive(Serialize)]
18 struct Body {
19 error: &'static str,
20 }
21 (self.status, Json(Body { error: self.message })).into_response()
22 }
23}
24
25pub struct AuthUser {
26 pub id: i64,
27 pub scopes: Vec<String>,
28}
29
30#[async_trait]
31impl FromRequestParts<AppState> for AuthUser {
32 type Rejection = AuthRejection;
33
34 async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
35 let header = parts
36 .headers
37 .get(header::AUTHORIZATION)
38 .and_then(|v| v.to_str().ok())
39 .ok_or(AuthRejection {
40 status: StatusCode::UNAUTHORIZED,
41 message: "missing authorization header",
42 })?;
43
44 let token = header.strip_prefix("Bearer ").ok_or(AuthRejection {
45 status: StatusCode::UNAUTHORIZED,
46 message: "expected bearer token",
47 })?;
48
49 let claims = state.jwt.verify(token).map_err(|_| AuthRejection {
50 status: StatusCode::UNAUTHORIZED,
51 message: "invalid or expired token",
52 })?;
53
54 state
55 .users
56 .find_active(claims.sub)
57 .await
58 .map_err(|_| AuthRejection {
59 status: StatusCode::INTERNAL_SERVER_ERROR,
60 message: "failed to load user",
61 })?
62 .ok_or(AuthRejection {
63 status: StatusCode::FORBIDDEN,
64 message: "account disabled",
65 })
66 .map(|record| AuthUser {
67 id: record.id,
68 scopes: claims.scopes,
69 })
70 }
71}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Implementing FromRequestParts lets handlers receive validated data as a plain typed argument.
- 2A dedicated rejection type with IntoResponse keeps each failure mapped to a precise status and message.
- 3The ? operator chains fallible steps so any auth failure short-circuits into a clean HTTP response.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
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/custom-auth-extractors-in-axum-explained-rust-55d1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.