[dependencies]jsonwebtoken = { version = "=11.1.0", features = ["rust_crypto"] }reqwest = { version = "=0.13.5", features = ["blocking", "json"] }serde = { version = "=1.0.229", features = ["derive"] }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) }}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
reqwest::blocking::get(url)?.error_for_status()?.json()deserializes the JWKS straight intojsonwebtoken::jwk::JwkSet.decode_header(token)reads the header without verifying it — only to get thekid;JwkSet::find(&kid)returns the matching JWK.DecodingKey::from_jwkbuilds the verification key, anddecodechecks the ES256 signature,exp,issandaud.Validation::new(Algorithm::ES256)accepts only ES256;set_required_spec_claimsmakesexp,issandaudmandatory.
Gotchas
- jsonwebtoken validates
audby default: a token with anaudfails withInvalidAudienceuntilset_audiencenames yours. That is the safe default — set the audience rather than switchingvalidate_audoff. - The JWKS here is fetched once. A long-running service needs to refresh it periodically and when
findmisses, with a rate limit so inventedkidvalues 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,audandexpare checked byValidationwhether or not they are fields of the claims struct.