Cycle de vie d'une requête
Ce document suit le chemin d'une requête HTTP depuis l'entrée dans Axum jusqu'à la réponse, en se concentrant sur les flux d'authentification. L'objectif est de comprendre pourquoi le code est organisé ainsi, pas seulement comment l'appeler.
Le voyage d'une requête authentifiée
sequenceDiagram
participant C as Client
participant R as Routeur Axum
participant EXT as Extension AuthConfig
participant BL as JwtBlacklistService
participant JWT as JwtService
participant AU as Extracteur AuthUser
participant H as Handler
participant SVC as Service métier
participant DB as PostgreSQL
C->>R: requête HTTP (Authorization: Bearer <jwt> ou cookie jwt_token)
R->>EXT: récupère Arc<AuthConfig> depuis les extensions Axum
R->>BL: is_blacklisted(SHA256(token))
BL->>DB: SELECT FROM jwt_blacklist WHERE hash = ?
DB-->>BL: résultat
BL-->>R: false (non révoqué)
R->>JWT: validate_token(auth_config, token)
JWT-->>R: Claims { sub, jti, username, roles, permissions, kind }
R->>AU: from_request_parts → AuthUser { user_id, username, roles, permissions }
AU->>H: handler reçoit AuthUser en paramètre
H->>SVC: appel service
SVC->>DB: requête SeaORM
DB-->>SVC: données
SVC-->>H: résultat métier
H-->>C: réponse JSON (200 OK)
La validation du token ne touche la base de données qu'une seule fois (vérification blacklist). La vérification de signature JWT est purement en mémoire — c'est intentionnel pour la scalabilité.
Login — email + mot de passe
Endpoint : POST /api/v1/auth/login
Handler : login
sequenceDiagram
participant C as Client
participant H as Handler login
participant RL as RateLimitService
participant AS as AuthService
participant US as UserService
participant TOTP as TotpService
participant JWT as JwtService
participant RT as RefreshTokenService
participant HK as RustwardenHooks
participant DB as PostgreSQL
C->>H: POST /api/v1/auth/login {username, password, remember_me?}
H->>RL: check_auth_rate_limit (5 req/min par IP)
alt limite atteinte
RL-->>C: 429 Too Many Requests
end
H->>AS: authenticate_user(db, username, password)
AS->>DB: SELECT user WHERE username = ?
DB-->>AS: user model
AS->>AS: bcrypt::verify(password, hash)
alt credentials invalides
AS-->>C: 401 Unauthorized (message générique)
end
AS-->>H: User
H->>H: vérifier email_verified (si requis par AppSettings)
H->>TOTP: is_totp_enabled(db, user_id)
alt 2FA activé
TOTP->>DB: create_challenge(user_id)
DB-->>TOTP: challenge_token
TOTP-->>C: 200 { requires_2fa: true, challenge_token }
Note over C: le client fait ensuite POST /api/v1/auth/2fa/verify
end
H->>US: get_user_roles(db, user_id)
DB-->>US: roles[]
H->>JWT: generate_token(auth_config, user_id, username, roles, permissions)
JWT-->>H: access_token (JWT signé HS256)
H->>RT: create_refresh_token(db, user_id, expiration_days)
RT->>DB: INSERT refresh_token (hash SHA-256)
DB-->>RT: CreatedRefreshToken
RT-->>H: plain_token (envoyé au client)
H->>H: construire cookie jwt_token (HttpOnly, Secure, SameSite=Strict)
H->>HK: on_user_login(db, user_id, username)
H-->>C: 200 LoginResponse + Set-Cookie: jwt_token
Points notables du flux de login :
AuthService::authenticate_userutilisebcrypt::verifyen temps constant pour éviter les attaques temporelles.- Les messages d'erreur sont intentionnellement génériques (pas de distinction "utilisateur inconnu" / "mot de passe incorrect").
- Le cookie
jwt_tokenestHttpOnly— JavaScript ne peut pas le lire. Le body JSON contient aussi le token pour les clients API (mobile, SPA avec storage explicite). remember_me: trueallonge la durée du refresh token (remember_me_expiration_days) sans modifier l'access token.
Flux 2FA
Si TotpService::is_totp_enabled retourne true, le handler n'émet pas de JWT. Il crée un challenge temporaire et retourne { requires_2fa: true, challenge_token }. Le client doit ensuite appeler POST /api/v1/auth/2fa/verify avec le code TOTP à 6 chiffres ou un code de récupération.
Refresh token
Endpoint : POST /api/v1/auth/refresh
Handler : refresh_token
sequenceDiagram
participant C as Client
participant H as Handler refresh_token
participant RL as RateLimitService
participant RT as RefreshTokenService
participant US as UserService
participant JWT as JwtService
participant DB as PostgreSQL
C->>H: POST /api/v1/auth/refresh { refresh_token: "abc123..." }
H->>RL: check_auth_rate_limit
H->>RT: validate_refresh_token(db, plain_token)
RT->>RT: SHA256(plain_token) → hash
RT->>DB: SELECT WHERE hash = ? AND NOT revoked AND NOT expired
DB-->>RT: refresh_token model
RT->>RT: has_been_used() ?
alt token déjà utilisé (attaque de réutilisation)
RT->>DB: revoke_all_tokens_for_user (sécurité maximale)
RT-->>C: 401 TokenReused — toutes les sessions révoquées
end
RT-->>H: old_token model
H->>US: get_user_by_id(db, old_token.user_id)
DB-->>US: user
H->>US: get_user_roles + get_permissions
H->>JWT: generate_token(auth_config, …)
JWT-->>H: nouveau access_token
H->>RT: create_refresh_token(db, user_id, expiration_days)
DB-->>RT: new CreatedRefreshToken
H->>RT: mark_as_replaced(db, old_id, new_id)
DB-->>RT: OK
H-->>C: 200 RefreshResponse { access_token, refresh_token, expires_in } + Set-Cookie
La rotation automatique signifie que chaque refresh token n'est utilisable qu'une seule fois. Après usage, il est marqué replaced et un nouveau token est émis. Si le serveur détecte une tentative de réutilisation d'un token déjà remplacé, il révoque toutes les sessions de l'utilisateur — signal fort d'un vol de token.
Logout
Endpoint : POST /api/v1/auth/logout
Handler : logout
sequenceDiagram
participant C as Client
participant H as Handler logout
participant JWT as JwtService
participant BL as JwtBlacklistService
participant RT as RefreshTokenService
participant HK as RustwardenHooks
participant DB as PostgreSQL
C->>H: POST /api/v1/auth/logout (Authorization: Bearer ou cookie)
H->>H: extraire JWT (priorité : header > cookie)
H->>JWT: validate_token(auth_config, jwt)
JWT-->>H: Claims { sub: user_id, ... }
H->>BL: add_to_blacklist(db, jwt, user_id)
BL->>BL: SHA256(jwt) → hash
BL->>DB: INSERT jwt_blacklist (hash, user_id, expires_at)
H->>RT: revoke_all_tokens_for_user(db, user_id)
DB-->>RT: nombre de tokens révoqués
H->>HK: on_user_logout(db, user_id)
H->>H: construire cookie jwt_token vide (MaxAge=0)
alt requête HTML (Accept: text/html)
H-->>C: 302 Redirect /login + Set-Cookie: jwt_token= (expiré)
else requête API
H-->>C: 200 { message, tokens_revoked } + Set-Cookie: jwt_token= (expiré)
end
Le logout effectue deux opérations complémentaires : blacklister l'access token courant (révocation immédiate, même si le JWT n'est pas encore expiré) ET révoquer tous les refresh tokens de l'utilisateur. La réponse est adaptée selon le header Accept — redirect pour les formulaires HTML, JSON pour les clients API.
Extraction d'utilisateur dans un handler
L'extracteur AuthUser est le mécanisme standard pour accéder à l'identité de l'appelant dans un handler Axum.
use axum::{routing::get, Router, Json}; use rustwarden_core::auth::extractors::AuthUser; async fn get_profile(user: AuthUser) -> Json<serde_json::Value> { Json(serde_json::json!({ "user_id": user.user_id, "username": user.username, "roles": user.roles, "permissions": user.permissions, })) } let app: Router = Router::new().route("/profile", get(get_profile));
Ce que fait AuthUser en coulisse (voir extractors.rs) :
- Cherche
Authorization: Bearer <token>dans les headers — priorité absolue. - Si absent, cherche le cookie
jwt_token(HttpOnly, posé au login). - Lit
Arc<AuthConfig>depuis les extensions Axum (chargé une fois au démarrage). - Appelle
JwtService::validate_token— vérification de signature et d'expiration. - Consulte
JwtBlacklistService::is_blacklisted— vérification de révocation. - Construit
AuthUser { user_id, username, roles, permissions }depuis lesClaims.
Si l'une de ces étapes échoue, Axum retourne 401 Unauthorized automatiquement — le handler n'est jamais appelé.
Vérification de rôle ou de permission dans un handler :
async fn admin_only(user: AuthUser) -> Result<Json<serde_json::Value>, StatusCode> { if !user.has_role("admin") { return Err(StatusCode::FORBIDDEN); } // Le rôle "admin" implique toutes les permissions (has_permission retourne true) Ok(Json(serde_json::json!({ "ok": true }))) } async fn check_permission(user: AuthUser) -> Result<Json<serde_json::Value>, StatusCode> { if !user.has_permission("users.list") { return Err(StatusCode::FORBIDDEN); } Ok(Json(serde_json::json!({ "ok": true }))) }
Diagramme d'état du JWT
stateDiagram-v2
[*] --> Generated : JwtService::generate_token()
Generated --> Active : émis au client\n(login ou refresh)
Active --> Expired : exp < now\n(automatique)
Active --> Blacklisted : logout explicite\nJwtBlacklistService::add_to_blacklist()
Active --> Revoked : révocation globale\n(réutilisation refresh token détectée)
Blacklisted --> [*] : nettoyage périodique\n(jwt_blacklist_cleanup task)
Expired --> [*]
Revoked --> [*]
note right of Active
Durée configurable via
JWT_EXPIRATION_MINUTES
(défaut: 15 min)
end note
note right of Blacklisted
Hash SHA-256 stocké en DB
jusqu'à l'expiration naturelle
end note
Un token Blacklisted reste dans la table jwt_blacklist jusqu'à sa date d'expiration naturelle. La tâche de fond jwt_blacklist_cleanup purge les entrées expirées pour limiter la croissance de la table.
Bearer vs Cookie — tableau comparatif
| Critère | Authorization: Bearer | Cookie jwt_token |
|---|---|---|
| Client cible | API mobile, SPA avec storage, services inter-serveurs | Navigateur web (formulaires HTML, HTMX) |
| Stockage côté client | localStorage / sessionStorage / keychain | Cookie HttpOnly (inaccessible à JS) |
| Vulnérabilité XSS | Exposé si stocké dans localStorage | Protégé par HttpOnly |
| Vulnérabilité CSRF | Immunisé (header custom) | Nécessite le middleware CSRF |
| Priorité d'extraction | Prioritaire — vérifié en premier | Fallback si header absent |
| Posé automatiquement | Non — le client doit stocker et envoyer | Oui — Set-Cookie au login |
| Effacé au logout | Non — le client doit supprimer | Oui — Set-Cookie: jwt_token=; MaxAge=0 |
La priorité Bearer > Cookie est délibérée : un client API peut toujours surcharger le cookie en envoyant un header explicite, par exemple pour tester avec un token de test sans toucher aux cookies du navigateur.
Recommandation pratique :
- SPA découplée ou application mobile : utiliser
Authorization: Bearerexclusivement, stocker le token en mémoire (pas localStorage) ou dans un stockage sécurisé natif. - Interface HTML rendue côté serveur (HTMX, formulaires) : laisser le framework gérer le cookie — rien à faire côté client.
- Services inter-serveurs :
Authorization: Beareravec un Personal Access Token (/api/v1/auth/tokens).
Flux du jwt_extractor
Le module jwt_extractor centralise la logique d'extraction du token brut depuis la requête. Il est utilisé par l'extracteur AuthUser, le middleware auth_required, et le handler logout — tous trois appliquent la même règle : header avant cookie.
jwt_extractor::extract_jwt_from_request(request) ├── Authorization: Bearer <token> → extract_bearer_token(header) └── Cookie: jwt_token=<token> → cookie_jar.get("jwt_token")
Si ni l'un ni l'autre n'est présent, la fonction retourne une erreur, et le middleware décide de la réponse (401 pour l'extracteur API, redirect /login pour le middleware HTML).
GitRust