API d'authentification JWT
Le module JWT autonome signe et vérifie un seul token. Une vraie
API a besoin de tout le cycle de vie : un token d'accès à courte durée, un
token de rafraîchissement à longue durée, la rotation au rafraîchissement, et
la révocation pour la déconnexion. Rustango fournit tout cela sous la
forme de JwtLifecycle — et un routeur clé en main qui monte pour vous
POST /api/auth/login, /refresh, /logout, et GET /me.
Source :
rustango::tenancy::jwt_lifecycle(JwtLifecycle,JwtTokenPair,JwtClaims) etrustango::tenancy::auth_routes(jwt_router,Config) +rustango::jti_store(JtiStore,InMemoryJtiStore) — derrièrejwt+tenancy.Version exécutable : le moteur de tokens est couvert par le test
auth_demo—cargo test -p auth_demo --test auth_jwt_api. Les endpoints HTTP sont cloisonnés par tenant et éprouvés de bout en bout par le proprecrates/rustango/tests/tenant_auth_live.rsdu framework.
Un terme vous est inconnu ? token d'accès/de rafraîchissement, rotation, révocation — voir le glossaire.
Complément approfondi de la section « Émettre et rafraîchir des JWT » du Guide de sécurité. Pour un token unique géré manuellement, voir plutôt JWT (autonome).
Table des matières
- Le routeur intégré · Le câblage
- Le moteur de tokens · Rafraîchissement et rotation
- Révocation et le magasin de JTI · Claims personnalisés
- Notes et limites
Le routeur intégré
jwt_router monte les quatre endpoints standard sur la table rustango_users
propre à chaque tenant — les ~50 lignes de boilerplate de login que tout projet
réécrit sinon :
| Méthode | Chemin | Corps / Auth | Renvoie |
|---|---|---|---|
| POST | /api/auth/login | {username, password} | {access, refresh, user} |
| POST | /api/auth/refresh | {refresh} | {access, refresh} |
| POST | /api/auth/logout | Authorization: Bearer <access> | 204 (révoque le JTI) |
| GET | /api/auth/me | Authorization: Bearer <access> | {user_id, username, is_superuser} |
Login vérifie le mot de passe avec argon2id, puis émet une
paire. Les chemins, TTL et la clé de signature sont configurables via Config.
Le câblage
use rustango::tenancy::auth_routes::{jwt_router, Config};
rustango::manage::Cli::new()
.tenancy()
.api(my_app::urls::api()
.merge(jwt_router(Config::default()))) // monte /api/auth/*
.run()
.await
Config::default() signe avec RUSTANGO_SESSION_SECRET (la même clé que le
cookie de session admin) et utilise des TTL de 15 min pour l'accès / 7 jours pour
le rafraîchissement. Redéfinissez prefix, access_ttl_secs,
refresh_ttl_secs, ou session_secret selon vos besoins. Les endpoints
s'exécutent dans le contexte du tenant, montez-les donc dans une application
tenancy.
# Login → access + refresh
curl -sX POST localhost:8080/api/auth/login \
-H 'content-type: application/json' \
-d '{"username":"alice","password":"hunter2hunter"}'
# Appeler un endpoint protégé
curl localhost:8080/api/auth/me -H "Authorization: Bearer $ACCESS"
Le moteur de tokens (JwtLifecycle)
Sous le routeur se trouve JwtLifecycle — utilisable directement si vous voulez
le cycle de vie sans la forme HTTP intégrée :
use rustango::tenancy::jwt_lifecycle::JwtLifecycle;
let jwt = JwtLifecycle::new(secret_32_bytes);
// Login : émettre la paire.
let pair = jwt.issue_pair(user_id);
// → pair.access (TTL court, à envoyer dans l'en-tête Authorization)
// → pair.refresh (TTL long, à stocker dans un cookie HttpOnly / stockage sécurisé)
// Requête authentifiée : vérifier le token d'accès.
match jwt.verify_access(&access) {
Some(claims) => { /* claims.sub est l'id utilisateur */ }
None => { /* 401 : invalide, expiré, révoqué, ou mauvais type */ }
}
Les tokens d'accès et de rafraîchissement ne sont pas interchangeables —
verify_access rejette un token de rafraîchissement et vice versa, de sorte
qu'un token d'accès à courte durée volé ne peut pas servir à en forger de
nouveaux :
let pair = jwt.issue_pair(42);
assert!(jwt.verify_refresh(&pair.access).is_none());
assert!(jwt.verify_access(&pair.refresh).is_none());
Rafraîchissement et rotation
refresh échange un token de rafraîchissement valide contre une nouvelle
paire et met en liste noire le JTI de l'ancien token de rafraîchissement —
expiration glissante avec des tokens de rafraîchissement à usage unique (le
rejeu de l'ancien est refusé) :
let pair = jwt.issue_pair(7);
let rotated = jwt.refresh(&pair.refresh).expect("refresh ok");
assert_ne!(pair.access, rotated.access);
assert!(jwt.refresh(&pair.refresh).is_none()); // l'ancien refresh est maintenant mort
Par défaut, refresh préserve les claims personnalisés du token. Si les
permissions ont pu changer (rôle révoqué, portée réduite), utilisez
refresh_with(token, new_claims) pour substituer un payload frais tout en
mettant quand même en liste noire l'ancien JTI de rafraîchissement.
Révocation et le magasin de JTI
Chaque token porte un jti unique. revoke l'ajoute à une liste noire de sorte
que les appels verify_* suivants échouent jusqu'à ce que le token ait de toute
façon expiré — c'est ce qu'appelle POST /api/auth/logout :
let pair = jwt.issue_pair(1);
assert!(jwt.revoke(&pair.access));
assert!(jwt.verify_access(&pair.access).is_none());
La liste noire réside dans un JtiStore interchangeable. Le InMemoryJtiStore
par défaut est mono-processus et perd les révocations au redémarrage —
convient pour une seule instance. Tout déploiement multi-réplica DOIT installer
un magasin partagé et durable (Redis / BD) pour qu'une déconnexion sur une
réplica soit honorée par toutes :
use rustango::jti_store::{InMemoryJtiStore, JtiStore};
use std::sync::Arc;
let shared: Arc<dyn JtiStore> = Arc::new(InMemoryJtiStore::new()); // à remplacer par Redis en prod
let a = JwtLifecycle::new(secret.clone()).with_jti_store(Arc::clone(&shared));
let b = JwtLifecycle::new(secret).with_jti_store(Arc::clone(&shared));
let pair = a.issue_pair(5);
a.revoke(&pair.access);
assert!(b.verify_access(&pair.access).is_none()); // B voit la révocation de A
Sans magasin partagé,
/logoutest au mieux « best-effort » : un token révoqué peut encore être accepté sur une autre réplica jusqu'à son expiration naturelle. C'est le paramètre de production le plus important pour l'authentification JWT.
Claims personnalisés
Intégrez roles / tenant / scope directement dans le token pour que la
vérification ne nécessite aucune consultation de BD. Les noms réservés (sub,
exp, jti, typ) sont rejetés :
let custom = serde_json::json!({ "roles": ["admin"], "tenant": "acme" })
.as_object().unwrap().clone();
let pair = jwt.issue_pair_with(99, custom)?;
let claims = jwt.verify_access(&pair.access).unwrap();
let roles: Vec<String> = claims.get_custom("roles").unwrap(); // ["admin"]
Les claims personnalisés survivent à refresh (reportés sur la nouvelle paire)
sauf si vous utilisez refresh_with.
Notes et limites
- Sessions vs JWT vs ceci : un JWT simple ne peut pas être
révoqué ; une Session est révocable mais nécessite une
consultation du magasin à chaque requête ;
JwtLifecycleest la voie intermédiaire — vérification sans état, plus une liste de blocage JTI pour les révocations dont vous avez réellement besoin (déconnexion, rotation). - Les endpoints HTTP sont cloisonnés par tenant.
jwt_routerrésout les utilisateurs via le contexte du tenant +rustango_users; montez-le dans une application.tenancy(). Le moteur de tokens (JwtLifecycle) lui-même n'a pas cette exigence. - Associez ceci au
JwtBackendde la chaîne de backends d'authentification pour authentifier des routes arbitraires à partir de l'en-têteAuthorization: Bearer. - Signature HS256, plancher de clé de 32 octets — même algorithme et mêmes contraintes que le JWT autonome.
