Skip to content
rusthacks.dev
Security

JWT Validation in Rust Without the Usual Mistakes Explained

The mistakes that make JWT checks worthless: accepting the none algorithm, trusting the header's alg, skipping exp and aud, and mixing key types. Validate correctly with the jsonwebtoken crate.

3 min read
Summarize in AI
Flat illustration for the post “JWT Validation in Rust Without the Usual Mistakes Explained”, composed around security.

A JWT looks like proof of identity, and it is only as good as the check on the server. Most JWT breaches are not broken cryptography. They are validation code that accepts a token it should have rejected: one signed with none, one expired last year, one meant for a different service. This post shows the correct call in Rust and each mistake by name. It belongs to the authentication posts of security for Rust developers, and its web side is in Axum middleware.

jsonwebtoken 9, Rust 1.90.

The correct check

Cargo.tomlTOML
[dependencies]
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }
rust
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
    iss: String,
    aud: String,
}

fn main() {
    let secret = b"change-me-use-32-random-bytes!!!";
    let claims = Claims {
        sub: "user-7".into(),
        exp: 4_102_444_800, // far future, for the example
        iss: "https://auth.example.test".into(),
        aud: "my-api".into(),
    };
    let token = encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret)).unwrap();

    let mut validation = Validation::new(Algorithm::HS256);
    validation.set_audience(&["my-api"]);
    validation.set_issuer(&["https://auth.example.test"]);
    validation.set_required_spec_claims(&["exp", "aud", "iss", "sub"]);

    let data = decode::<Claims>(&token, &DecodingKey::from_secret(secret), &validation);
    println!("{:?}", data.map(|d| d.claims.sub));
}

It prints Ok("user-7"). The Validation fixes HS256, requires four claims, and checks audience and issuer. decode checks the signature and exp (with a default leeway of 60 seconds). Change the audience to other-api and the result is Err(InvalidAudience).

The mistakes

MistakeWhy it is dangerousPrevention
Trusting the alg headernone, or HS256 with an RSA public key as secretValidation::new(Algorithm::...) with a fixed list
Skipping expstolen tokens work foreverrequired claims, short lifetime
Not checking auda token for service A works on Bset the audience
Not checking isstokens from any issuer with a key you trustset the issuer
Decoding without verifyingforged claimsuse decode, never dangerous_* for decisions
Long-lived access tokensno revocation5 to 15 minutes plus refresh tokens
Sensitive data in the payloadpayload is only base64keep PII out

Asymmetric keys and rotation

When an identity provider signs with RSA or ECDSA, you verify with its public key, fetched from its JWKS endpoint. Read kid from the header only to select a key from your trusted set, never to fetch arbitrary key material:

rust
let header = jsonwebtoken::decode_header(&token)?;
let key = keys.get(header.kid.as_deref().unwrap_or(""))
    .ok_or("unknown key id")?;

Cache the key set and refresh it on an unknown kid with a rate limit, so an attacker cannot force requests to your identity provider. Pin the algorithm to the key's type: RS256 keys with Algorithm::RS256.

In Axum

Write an extractor that reads Authorization: Bearer, calls decode, and returns 401 on failure. Do it once, as in extractors, and return the same generic error for every failure, so responses do not reveal which check failed.

Limits

The library validates tokens; it does not manage login, refresh tokens or revocation. The standard is RFC 8725, the JWT Best Current Practices, and the crate documentation is at docs.rs/jsonwebtoken. A decoder for inspecting tokens by hand is the JWT decoder tool.

Also inWeb
Taggedjwt
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.