Authentification JWT

JWT (JSON Web Token) est un standard ouvert (RFC 7519) permettant de transmettre des informations de manière sécurisée entre différentes parties. Il s'agit d'une méthode compacte et sûre pour les URL de représenter des revendications au format d'objet JSON, couramment utilisée pour l'authentification et l'échange d'informations.

Un JWT se compose de trois parties :

  • En-tête - Spécifie le type de jeton et l'algorithme de signature utilisé
  • Charge utile - Contient les revendications, telles que l'ID utilisateur, les rôles, la date d'expiration, etc.
  • Signature - Utilisée pour vérifier que le message n'a pas été altéré pendant la transmission

Utilisation typique du JWT dans le flux d'authentification :

  1. Après la connexion de l'utilisateur, le serveur génère un jeton JWT
  2. Le jeton est renvoyé au client et stocké (généralement dans localStorage ou les cookies)
  3. Dans les requêtes suivantes, le client inclut ce jeton dans l'en-tête Authorization
  4. Le serveur valide l'authenticité du jeton et accorde l'accès

Fournit un middleware pour l'authentification JWT Auth.

Choisir un fournisseur cryptographique

Salvo 0.95 propose deux features JWT alternatives :

  • jwt-auth utilise le fournisseur AWS-LC.
  • jwt-auth-ring utilise le fournisseur RustCrypto.

Dans une compilation normale, n'en activez qu'une seule. La feature ring distincte sélectionne ring pour l'intégration rustls de Salvo et n'active pas l'authentification JWT. L'exemple ci-dessous utilise jwt-auth. Pour utiliser RustCrypto à la place :

salvo = { version = "0.95", default-features = false, features = ["server", "http1", "ring", "jwt-auth-ring"] }

Si l'unification des features Cargo active les deux fournisseurs JWT, appelez salvo::jwt_auth::install_crypto_provider() au tout début de main, avant toute opération directe avec jsonwebtoken ou configuration du middleware JWT. L'exemple ci-dessous le fait afin de garantir une sélection déterministe.

Exemple de Code

main.rs
Cargo.toml
use jsonwebtoken::{self, EncodingKey};
use salvo::http::{Method, StatusError};
use salvo::jwt_auth::{ConstDecoder, QueryFinder};
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime};

const SECRET_KEY: &str = "YOUR SECRET_KEY";

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct JwtClaims {
    username: String,
    exp: i64,
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();
    salvo::jwt_auth::install_crypto_provider()
        .expect("install the JWT crypto provider before first use");

    let auth_handler: JwtAuth<JwtClaims, _> =
        JwtAuth::new(ConstDecoder::from_secret(SECRET_KEY.as_bytes()))
            .finders(vec![
                // Box::new(HeaderFinder::new()),
                Box::new(QueryFinder::new("jwt_token")),
                // Box::new(CookieFinder::new("jwt_token")),
            ])
            .force_passed(true);

    let acceptor = TcpListener::new("0.0.0.0:8698").bind().await;
    Server::new(acceptor)
        .serve(Router::with_hoop(auth_handler).goal(index))
        .await;
}
#[handler]
async fn index(req: &mut Request, depot: &mut Depot, res: &mut Response) -> anyhow::Result<()> {
    if req.method() == Method::POST {
        let (username, password) = (
            req.form::<String>("username").await.unwrap_or_default(),
            req.form::<String>("password").await.unwrap_or_default(),
        );
        if !validate(&username, &password) {
            res.render(Text::Html(LOGIN_HTML));
            return Ok(());
        }
        let exp = OffsetDateTime::now_utc() + Duration::days(14);
        let claim = JwtClaims {
            username,
            exp: exp.unix_timestamp(),
        };
        let token = jsonwebtoken::encode(
            &jsonwebtoken::Header::default(),
            &claim,
            &EncodingKey::from_secret(SECRET_KEY.as_bytes()),
        )?;
        res.render(Redirect::other(format!("/?jwt_token={token}")));
    } else {
        match depot.jwt_auth_state() {
            JwtAuthState::Authorized => {
                let data = depot.jwt_auth_data::<JwtClaims>().unwrap();
                res.render(Text::Plain(format!(
                    "Hi {}, have logged in successfully!",
                    data.claims.username
                )));
            }
            JwtAuthState::Unauthorized => {
                res.render(Text::Html(LOGIN_HTML));
            }
            JwtAuthState::Forbidden => {
                res.render(StatusError::forbidden());
            }
        }
    }
    Ok(())
}

fn validate(username: &str, password: &str) -> bool {
    username == "root" && password == "pwd"
}

static LOGIN_HTML: &str = r#"<!DOCTYPE html>
<html>
    <head>
        <title>JWT Auth Demo</title>
    </head>
    <body>
        <h1>JWT Auth</h1>
        <form action="/" method="post">
        <label for="username"><b>Username</b></label>
        <input type="text" placeholder="Enter Username" name="username" required>

        <label for="password"><b>Password</b></label>
        <input type="password" placeholder="Enter Password" name="password" required>

        <button type="submit">Login</button>
    </form>
    </body>
</html>
"#;