Backends d'authentification
Un backend d'authentification répond à une seule question : étant donné une
requête entrante, qui est l'utilisateur ? Rustango vous permet d'en empiler
plusieurs — HTTP Basic, clé d'API, JWT — dans une chaîne que le middleware
d'authentification essaie dans l'ordre, de sorte qu'une même application peut
accepter humains et machines sur les mêmes routes. La chaîne n'est qu'une
liste de configuration, câblée à axum. Associez-la à
require_auth / require_perm pour verrouiller les routes et à l'extracteur
CurrentUser pour lire le résultat.
Un terme vous est inconnu ici ? Backend, middleware, extracteur, codename de permission — voir le glossaire.
Source :
rustango::tenancy::auth_backends(AuthBackend,ModelBackend,ApiKeyBackend,JwtBackend,AuthUser,AuthError) etrustango::tenancy::{RouterAuthExt, CurrentUser}— derrière la fonctionnalitétenancy. Un registre portable et indépendant de la base de données vit aussi àrustango::auth_backends(toujours compilé).Version exécutable : chaque extrait est copié depuis
auth_backends_doc.rs(cargo test -p rustango --features sqlite,tenancy --test auth_backends_doc).
Table des matières
- La chaîne · Les backends intégrés
- Verrouiller les routes : require_auth
- Lire l'utilisateur : CurrentUser
- Permissions : require_perm
- Le registre portable
- Voir aussi
La chaîne
Vous passez à require_auth un Vec<Arc<dyn AuthBackend>>. À chaque requête, le
middleware les essaie dans l'ordre :
- le premier backend qui reconnaît l'identifiant l'emporte (renvoie l'utilisateur) ;
- un backend qui ne le reconnaît pas renvoie « aucun » et le suivant est essayé ;
- si un backend échoue durement (par ex. un compte inactif sur un jeton valide), la chaîne s'arrête avec cette erreur ;
- si aucun ne correspond, la requête reçoit un
401(avecrequire_auth) ou poursuit de manière anonyme (avecoptional_auth).
use std::sync::Arc;
use rustango::tenancy::auth_backends::{ApiKeyBackend, AuthBackend, ModelBackend};
let backends: Vec<Arc<dyn AuthBackend>> = vec![
Arc::new(ModelBackend), // HTTP Basic → humans
Arc::new(ApiKeyBackend), // Bearer key → machines
];
Les backends intégrés
| Backend | Identifiant qu'il lit | Identifie un utilisateur par |
|---|---|---|
ModelBackend | Authorization: Basic <base64(user:pass)> | nom d'utilisateur + vérification du mot de passe argon2id contre rustango_users |
ApiKeyBackend | Authorization: Bearer <prefix.secret> | la table rustango_api_keys (voir Clés d'API) |
JwtBackend | Authorization: Bearer <jwt> | un jeton HS256 signé (voir JWT) |
ApiKeyBackend et JwtBackend lisent tous deux Bearer et lèvent l'ambiguïté
par la forme (le premier segment séparé par un point d'une clé d'API fait
exactement 8 caractères). Construisez JwtBackend avec un secret d'au moins 32
octets (JwtBackend::new(secret) panique sinon) :
use rustango::tenancy::auth_backends::JwtBackend;
let backends: Vec<Arc<dyn AuthBackend>> = vec![
Arc::new(ModelBackend),
Arc::new(JwtBackend::new(jwt_secret_at_least_32_bytes.to_vec())),
];
JwtBackend accepte les tokens d'accès émis par JwtLifecycle et refuse ses
tokens de rafraîchissement — les deux sont identiques sur le fil à typ près,
donc un token de rafraîchissement présenté comme bearer serait une
authentification valant des jours au lieu de quelques minutes.
La révocation est optionnelle et désactivée par défaut. Un JwtBackend nu
ne consulte jamais de liste noire : un token révoqué par /api/auth/logout
continue donc de s'authentifier via ce backend jusqu'à sa propre expiration.
Partagez un magasin entre le cycle de vie et le backend pour que la déconnexion
prenne effet :
use rustango::jti_store::{InMemoryJtiStore, JtiStore};
let shared: Arc<dyn JtiStore> = Arc::new(InMemoryJtiStore::new()); // Redis en production
let lifecycle = JwtLifecycle::new(secret.clone()).with_jti_store(Arc::clone(&shared));
let backend = JwtBackend::new(secret).with_jti_store(Arc::clone(&shared));
Ce doit être le même magasin. En câbler deux a l'air configuré et n'applique rien : la déconnexion écrit dans l'un et la vérification lit l'autre. Voir révocation et le magasin de JTI.
Écrivez un backend personnalisé en implémentant le trait (une seule méthode async
qui inspecte les Parts de la requête et renvoie Option<AuthUser>) :
use async_trait::async_trait; // add `async-trait` to your Cargo.toml
use axum::http::request::Parts;
use rustango::sql::Pool;
use rustango::tenancy::auth_backends::{AuthBackend, AuthError, AuthUser};
struct HeaderBackend;
#[async_trait]
impl AuthBackend for HeaderBackend {
async fn authenticate(&self, parts: &Parts, _pool: &Pool)
-> Result<Option<AuthUser>, AuthError>
{
// ...inspect parts.headers, return Some(AuthUser{..}) or Ok(None)
Ok(None)
}
}
Verrouiller les routes : require_auth
RouterAuthExt ajoute le middleware. require_auth rejette les requêtes
anonymes avec un 401 ; optional_auth les laisse passer (de sorte qu'un
handler peut se ramifier selon connecté ou non) :
use rustango::tenancy::RouterAuthExt;
let app = Router::new()
.route("/profile", get(profile))
.require_auth(backends); // 401 if no backend matches
Comportement vérifié :
// no credentials → 401
// Basic alice:<correct> → 200
// Basic alice:<wrong> → 401 (no backend accepted; no enumeration)
// Bearer <valid api key> → 200
Lire l'utilisateur : CurrentUser
Les handlers lisent l'utilisateur authentifié avec l'extracteur CurrentUser.
Il est infaillible — Some(user) lorsqu'un backend en a résolu un, None sinon :
use rustango::tenancy::CurrentUser;
async fn profile(CurrentUser(user): CurrentUser) -> Response {
match user {
Some(u) => format!("hello {}", u.username).into_response(),
None => StatusCode::UNAUTHORIZED.into_response(),
}
}
Piège : parce que
CurrentUserest infaillible, oublierrequire_authne provoque pas d'échec de compilation — chaque requête voit simplementNone. Derrièrerequire_auth, les requêtes anonymes reçoivent déjà un401, doncusery est toujoursSome.
Permissions : require_perm
require_perm verrouille une route sur un codename de permission
({table}.{action}, par ex. post.add). Appliquez-le au sous-routeur interne et
require_auth à l'externe, afin que l'utilisateur soit résolu avant que la
permission ne soit vérifiée :
let admin = Router::new()
.route("/admin", get(admin_only))
.require_perm("post.add"); // inner: needs the codename
let app = Router::new()
.route("/profile", get(profile))
.merge(admin)
.require_auth(backends); // outer: resolves the user first
// alice (granted post.add) → /admin 200
// bob (authed, no grant) → /admin 403
// anonymous → /admin 401 (auth runs first)
Résolution : un superutilisateur (actif) passe tout ; un utilisateur
désactivé est refusé même avec des octrois ; une surcharge explicite par
utilisateur l'emporte sur les octrois de rôle ; sinon, tout rôle détenu par
l'utilisateur qui octroie le codename passe. Octroyez avec
set_user_perm_pool / les rôles via create_role_pool + assign_role_pool (les
tables de permissions sont créées par ensure_tables_pool).
Le registre portable
Séparément, rustango::auth_backends (à noter : racine de crate, et non
tenancy) est un petit registre indépendant du framework — une chaîne
Credentials → Principal dotée de son propre trait AuthBackend. Il n'a aucune
glu HTTP/axum ; utilisez-le lorsque vous voulez des backends d'authentification
interchangeables au sein de votre propre code d'authentification :
use rustango::auth_backends::{AuthBackendChain, Credentials, RemoteUserBackend};
let chain = AuthBackendChain::new().with(Arc::new(RemoteUserBackend::trust_username()));
let principal = chain.authenticate(&Credentials::remote("alice")).await?;
Mêmes sémantiques « le premier succès l'emporte / la première erreur arrête » que
la chaîne HTTP. Pour verrouiller de vraies routes, utilisez le middleware
tenancy ci-dessus.
Voir aussi
- Clés d'API et JWT — les identifiants que
ApiKeyBackend/JwtBackendconsomment. - Mots de passe — le hachage contre lequel
ModelBackendvérifie. - Décorateurs d'accès — verrouillage par handler
login_required/permission_required, l'alternative de style décorateur àrequire_auth/require_perm. - Sessions — authentification par cookie pour les navigateurs.
