Guide 3 min read Updated

Verify JWT in Rust (jsonwebtoken)

Verify JWT signatures in Rust with the jsonwebtoken crate. Sign and verify HS256 and RS256 tokens, validate iss/aud/exp claims, and verify JWTs against a JWKS endpoint with full Rust code examples.

Rust’s standard JWT library is the jsonwebtoken crate. It supports HS256/384/512 and RS256/384/512 natively; ES256/384 and EdDSA support varies by version — check the release notes for your pinned version before adopting those.

Install

[dependencies]
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }

Sign and verify an HS256 token

use jsonwebtoken::{encode, decode, EncodingKey, DecodingKey, Algorithm, Validation, Header};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    iss: String,
    aud: String,
    exp: usize,   // unix seconds — usize matches the crate's expectation
}

let secret = std::env::var("JWT_SECRET").unwrap(); // 32+ bytes

// Sign
let claims = Claims {
    sub: "user_123".into(),
    iss: "https://auth.example.com".into(),
    aud: "https://api.example.com".into(),
    exp: chrono::Utc::now().timestamp() as usize + 15 * 60,
};
let token = encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret.as_bytes()))?;

// Verify — hardcoded algorithm, explicit iss + aud
let mut validation = Validation::new(Algorithm::HS256);
validation.leeway = 60;                                          // 60s skew
validation.iss = Some("https://auth.example.com".to_string());
validation.aud = Some(std::collections::HashSet::from(["https://api.example.com".to_string()]));
validation.validate_exp = true;

let token_data = decode::<Claims>(&token, &DecodingKey::from_secret(secret.as_bytes()), &validation)?;
println!("{}", token_data.claims.sub); // "user_123"

Sign and verify an RS256 token

let private_pem = std::fs::read("rsa-private.pem")?;
let public_pem = std::fs::read("rsa-public.pem")?;

let token = encode(
    &Header::new(Algorithm::RS256),
    &claims,
    &EncodingKey::from_rsa_pem(&private_pem)?,
)?;

let token_data = decode::<Claims>(
    &token,
    &DecodingKey::from_rsa_pem(&public_pem)?,
    &Validation::new(Algorithm::RS256),
)?;

Verify a JWT against a JWKS endpoint

The jsonwebtoken crate does not ship a JWKS client. Use jwks-client (or fetch the JWKS yourself) and construct a DecodingKey from the matching JWK:

use jsonwebtoken::DecodingKey;

// Fetch + cache the JWKS (use jwks-client crate or your own cache with a TTL)
let jwks: serde_json::Value = reqwest::get("https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json").await?.json().await?;

// Find the key matching the token header kid
let header = jsonwebtoken::decode_header(&token)?;
let kid = header.kid.ok_or("missing kid")?;
let jwk = jwks["keys"].as_array().unwrap().iter()
    .find(|k| k["kid"] == kid).ok_or("unknown kid")?;

// Build an RSA DecodingKey from the JWK's n and e
let n = jwk["n"].as_str().unwrap();
let e = jwk["e"].as_str().unwrap();
let decoding_key = DecodingKey::from_rsa_components(n, e)?; // jsonwebtoken 9.2+

let token_data = decode::<Claims>(&token, &decoding_key, &Validation::new(Algorithm::RS256))?;

Cache the parsed DecodingKey objects in your process or a Redis-backed cache with a 1-hour TTL. Do not fetch the JWKS on every request.

Validation order

decode performs signature verification and exp/nbf validation in one call. iss and aud are checked when you populate the corresponding Validation fields. The crate does not validate iat — if you need a max-age check, compare iat to the current time yourself after a successful decode.


Continue reading