Skip to content
ecdsa.com

Verify a JWT with JWKS · Rust

How to verify a JWT with a JWKS in Rust

To verify a JWT against a JWKS in Rust, fetch the key set with reqwest into jsonwebtoken's JwkSet, find the JWK named by the token's kid, and decode with a Validation that fixes the algorithm, issuer, audience and required claims. The run serves a real JWKS over HTTPS inside the container and checks five tokens.

Tested with jsonwebtoken 11.1.0 (Rust 1.99.1, feature rust_crypto) and reqwest 0.13.5 (blocking client, rustls with the platform verifier).

Cargo.toml — jsonwebtoken and reqwest
[dependencies]
jsonwebtoken = { version = "=11.1.0", features = ["rust_crypto"] }
reqwest = { version = "=0.13.5", features = ["blocking", "json"] }
serde = { version = "=1.0.229", features = ["derive"] }
src/verifier.rs — key by kid, then decode with a strict Validation
use jsonwebtoken::jwk::JwkSet;
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use serde::Deserialize;
// Only the claims the application reads; exp, iss and aud are checked by Validation
#[derive(Debug, Deserialize)]
pub struct Claims {
pub sub: String,
}
pub struct Verifier {
jwks: JwkSet, // fetch once and cache; refetch on a schedule or on an unknown kid
validation: Validation,
}
impl Verifier {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let jwks: JwkSet = reqwest::blocking::get("https://issuer.example.com/.well-known/jwks.json")?
.error_for_status()?
.json()?;
let mut validation = Validation::new(Algorithm::ES256); // only ES256 is accepted
validation.set_issuer(&["https://issuer.example.com"]);
validation.set_audience(&["api://orders"]);
validation.set_required_spec_claims(&["exp", "iss", "aud"]);
Ok(Self { jwks, validation })
}
pub fn verify(&self, token: &str) -> Result<Claims, Box<dyn std::error::Error>> {
let kid = decode_header(token)?.kid.ok_or("token has no kid")?;
let jwk = self.jwks.find(&kid).ok_or_else(|| format!("no key with kid {kid:?} in the JWKS"))?;
let key = DecodingKey::from_jwk(jwk)?;
Ok(decode::<Claims>(token, &key, &self.validation)?.claims)
}
}
src/main.rs — check every token in tokens.txt
mod verifier;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let verifier = verifier::Verifier::new()?;
for line in std::fs::read_to_string("tokens.txt")?.lines() {
let (label, token) = line.split_once(' ').ok_or("bad line")?;
match verifier.verify(token) {
Ok(claims) => println!("{label}: accepted, sub={}", claims.sub),
Err(e) => println!("{label}: rejected: {e}"),
}
}
Ok(())
}

How it works

  1. reqwest::blocking::get(url)?.error_for_status()?.json() deserializes the JWKS straight into jsonwebtoken::jwk::JwkSet.
  2. decode_header(token) reads the header without verifying it — only to get the kid; JwkSet::find(&kid) returns the matching JWK.
  3. DecodingKey::from_jwk builds the verification key, and decode checks the ES256 signature, exp, iss and aud.
  4. Validation::new(Algorithm::ES256) accepts only ES256; set_required_spec_claims makes exp, iss and aud mandatory.

Gotchas

  • jsonwebtoken validates aud by default: a token with an aud fails with InvalidAudience until set_audience names yours. That is the safe default — set the audience rather than switching validate_aud off.
  • The JWKS here is fetched once. A long-running service needs to refresh it periodically and when find misses, with a rate limit so invented kid values cannot trigger a fetch per request.
  • reqwest 0.13 verifies TLS with rustls and the platform verifier, which reads the system trust store; that is why the test CA works here after update-ca-certificates.
  • Deserialize only the claims you use. iss, aud and exp are checked by Validation whether or not they are fields of the claims struct.

Related recipes