Middleware d'auth : résolution de l'AuthConfig et ordre des layers
Mode Diátaxis : explanation (comprendre la mécanique) + un mini how-to de troubleshooting à la fin. Référence pour les consommateurs de
rustwarden-corequi mappent leurs propres routes auth-required ou composent leur pile tower à la main.
Pourquoi cette page existe
Trois RFC se sont succédé pour stabiliser les middlewares
AuthService et
AdminService :
- RFC 0022 — fallback
AuthConfig::from_env()côté branche protected (correctifd4d6828). - RFC 0031 — fallback symétrique côté branche optional auth.
- RFC 0042 — les middlewares reçoivent l'
AuthConfigrésolue à la construction (train 1.2.0).
Les deux premières corrigeaient un symptôme : l'ordre des layers tower
configuré par défaut dans app.rs
place AdminService et AuthService avant le layer
Extension(Arc<AuthConfig>), donc au moment où ces middlewares
s'exécutent l'extension n'est pas encore dans request.extensions().
Sans fallback, le résultat était soit une erreur 500 silencieuse (avant
RFC 0022) soit des claims = None silencieux (avant RFC 0031).
Mais le fallback avait un effet de bord : from_env() ne connaît pas les
overrides du builder. .login_path(..), .auth_failure_mode(..) et la
bascule Json401 de .headless() n'atteignaient jamais les deux
middlewares. La RFC 0042 traite la cause : depuis 1.2.0, chaque middleware
porte l'Arc<AuthConfig> résolue par RustwardenBuilder::build().
Résolution de la configuration (depuis 1.2.0)
Les deux middlewares résolvent l'AuthConfig dans cet ordre :
- Configuration capturée à la construction (
with_auth_config). C'est la voie canonique :build()charge l'environnement, applique les overrides du builder, puis passe l'Arcaux constructeurs. Aucune dépendance à l'ordre des layers. Extension(Arc<AuthConfig>)lue dans la requête — chemin legacy, uniquement si le middleware a été construit avecnew(déprécié).AuthConfig::from_env()— chemin legacy de dernier recours, même condition. Untracing::warn!est émis une fois par processus pour signaler l'usage de l'API dépréciée.
Sur le chemin canonique, le middleware pose lui-même l'Arc capturé
dans request.extensions() avant de passer la main. Les extracteurs
internes (AuthUser, auth_required, admin_required, admin_helper)
voient donc exactement la même instance, y compris dans une pile composée
à la main sans layer Extension. Dans le câblage canonique, le layer
Extension(Arc<AuthConfig>) d'app.rs réinsère ensuite le même Arc :
c'est un no-op volontairement conservé.
Précédence des overrides
RustwardenBuilder::build() résout le mode d'échec ainsi :
.auth_failure_mode(..) > AUTH_FAILURE_MODE (env) > bascule .headless() (Json401) > Redirect
.login_path(..) prime sur LOGIN_PATH, qui prime sur /login. Une valeur
d'environnement explicite n'est pas un « défaut » : elle l'emporte sur la
bascule headless, ce qui permet un rollback sans redéploiement de code.
Les deux branches de AuthService
AuthService::call choisit son comportement en fonction du préfixe de
chemin demandé :
| Branche | Condition d'entrée | Mode | Réaction si JWT absent / invalide |
|---|---|---|---|
| Protected | path commence par un préfixe de protected_prefixes | Authentification obligatoire | auth_failure_mode : 303 vers login_path?redirect=… ou 401 {"error":"unauthorized"} |
| Optional auth | path ne matche aucun préfixe protégé | Authentification best-effort | Aucune action ; la requête traverse avec claims = None |
Dans les deux branches, dès qu'un JWT est présent, valide et non
blacklisté, les claims sont injectés dans request.extensions() :
Extension<Claims>(struct fortement typée)Extension<String>(JSON sérialisé, utile pour les templates Askama)
AdminService applique la même logique, avec vérification du rôle
admin, sur /admin* et /api/v1/admin*.
Composer sa propre pile tower
use std::sync::Arc; use rustwarden_core::config::auth::AuthConfig; use rustwarden_core::middleware::{admin_service::AdminService, auth_service::AuthService}; use tower::ServiceBuilder; let auth_config = Arc::new(AuthConfig::from_env()?); let prefixes = Arc::new(vec!["/api/private".to_string()]); let app = router.layer( ServiceBuilder::new() .layer_fn({ let (db, cfg) = (db.clone(), auth_config.clone()); move |s| AdminService::with_auth_config(s, db.clone(), cfg.clone()) }) .layer_fn({ let (db, cfg, p) = (db.clone(), auth_config.clone(), prefixes.clone()); move |s| AuthService::with_auth_config(s, db.clone(), p.clone(), cfg.clone()) }) .into_inner(), ); // Pas besoin de poser Extension(Arc<AuthConfig>) soi-même : le middleware le fait.
Exemple complet et compilable : app/examples/custom_auth_stack.rs.
Quand ai-je besoin de register_protected_prefix() ?
Le RustwardenBuilder expose register_protected_prefix(prefix) pour
ajouter un chemin à la liste protected_prefixes. Deux raisons
légitimes de l'utiliser :
-
Rejeter automatiquement les non-authentifiés sur certaines routes, selon
auth_failure_mode. Sans préfixe protégé, un visiteur anonyme atteindra le handler et il devra refuser lui-même viaExtension<Claims>(qui provoque un400 Bad Requestaxum) ou un check explicite. -
Activer les vérifications additionnelles (must-change-password, 2FA forcé global, blacklist JWT) qui ne tournent que sur la branche protected.
À l'inverse, pas besoin de déclarer un préfixe protégé juste pour que les claims soient injectés : depuis RFC 0031, la branche optional suffit.
Troubleshooting
« Mon app headless répond 303 vers /login au lieu de 401 »
Symptôme du bug corrigé par la RFC 0042 : avant 1.2.0, la bascule
Json401 de .headless() n'atteignait pas les middlewares. Depuis
1.2.0, headless() produit bien un 401 sauf si AUTH_FAILURE_MODE
ou .auth_failure_mode(..) disent autre chose. Si vous êtes encore sur
une version antérieure : posez AUTH_FAILURE_MODE=json401 dans
l'environnement.
« Mon app headless + SSR répond 401 au lieu de rediriger vers /login »
Conséquence attendue de la RFC 0042 pour une app .headless() qui
comptait sur l'ancien comportement (bug). Deux remèdes :
- dans le code :
.auth_failure_mode(AuthFailureMode::Redirect)sur le builder ; - sans redéploiement :
AUTH_FAILURE_MODE=redirectdans l'environnement.
« .login_path("/auth/signin") est ignoré »
Avant 1.2.0 : bug RFC 0042, seule la variable LOGIN_PATH était lue par
les middlewares. Depuis 1.2.0 : vérifiez que vous passez par
RustwardenApp::builder() ou, si vous composez votre pile, que vous
utilisez with_auth_config et non new (déprécié : cherchez le warning
deprecated constructor path dans les logs au premier appel).
« Login → /dashboard redirige vers /login sans erreur »
Symptôme historique du bug RFC 0031 : le cookie JWT est bien posé,
le user est authentifié, mais /dashboard (ou toute route hors des
préfixes protégés) redirige vers /login.
- Confirmer que le cookie est transmis :
curl -v -b /tmp/cookies.txt http://localhost:4000/dashboard 2>&1 | grep -E "HTTP|cookie"
- Vérifier les logs serveur pour
JWT extracted from cookie. - Vérifier la version de
rustwarden-core: antérieure à RFC 0031 → bumper, ou déclarerregister_protected_prefix("/dashboard")comme contournement.
« Le serveur renvoie 500 sur /admin sans log clair »
Sur une pile composée avec new (déprécié) et sans JWT_SECRET :
AuthConfig::from_env() échoue. Le log
AdminService: AuthConfig unavailable (no extension and from_env failed)
précise l'erreur. Passez à with_auth_config : la configuration est
validée une fois au démarrage et ce chemin disparaît.
Voir aussi
- RFC 0042 —
AuthConfigrésolue à la construction - RFC 0031 — fallback optional
- RFC 0022 — fallback protected (commit
d4d6828) - Sources :
auth_service.rs,admin_service.rs - Tests :
tests/auth_service_resolved_config.rs(RFC 0042),tests/middleware_authconfig_fallback.rs(chemin legacy) - Guide consommateur fédération :
fediverse-consumer-guide.md§4 « Wire-up »
GitRust