JWT Authentication

JWT (JSON Web Token) is an open standard (RFC 7519) for securely transmitting information between parties. It is a compact, URL-safe method of representing claims in JSON object format, commonly used for authentication and information exchange.

A JWT consists of three parts:

  • Header - Specifies the token type and the signing algorithm used
  • Payload - Contains claims, such as user ID, roles, expiration time, etc.
  • Signature - Used to verify that the message was not altered during transmission

Typical usage of JWT in authentication flow:

  1. After user login, the server generates a JWT token
  2. The token is returned to the client and stored (typically in localStorage or cookies)
  3. In subsequent requests, the client includes this token in the Authorization header
  4. The server validates the token's authenticity and grants access

Provides middleware for JWT Auth authentication.

Choosing a crypto provider

Salvo 0.95 provides two alternative JWT features:

  • jwt-auth uses the AWS-LC provider.
  • jwt-auth-ring uses the RustCrypto provider.

Enable only one in normal builds. The separate ring feature selects ring for Salvo's rustls integration; it does not enable JWT authentication. The example below uses jwt-auth. To use RustCrypto instead:

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

If Cargo feature unification enables both JWT providers, call salvo::jwt_auth::install_crypto_provider() at the very start of main, before any direct jsonwebtoken operation or JWT middleware setup. The example below does this so provider selection remains deterministic.

Example 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>
"#;