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
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
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
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
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
go
package auth import ( "net/http"
Setting and reading secure session cookies in Go
cookies
session-management
security
Intermediate
6 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
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/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.