Skip to content
ecdsa.com
Rust

jsonwebtoken (Rust)

Vincent Prouillet's jsonwebtoken crate, the most used JWT library for Rust: a closed Algorithm enum with no none, required exp, 60 seconds of default leeway, and — since version 10 — a crypto backend you must choose.
License
MIT
Checked version
11.1.0 ·
Source
github.com/Keats/jsonwebtoken
Documentation
docs.rs/jsonwebtoken/11.1.0/jsonwebtoken/

Compatibility at a glance

JOSE and JWT

JWS algorithms
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384EdDSA

jsonwebtoken encodes and decodes JWTs with encode, decode and a Validation object, using serde for the claims. Since version 10 the cryptography comes from a backend selected by feature flag — aws_lc_rs or rust_crypto — and the check used aws_lc_rs. Forgetting the feature does not fail the build: with default features only, 11.1.0 compiled and then panicked on the first encode() with Could not automatically determine the process-level CryptoProvider. Enable exactly one backend, as the sample's manifest does.

The algorithm set is a closed enum: HS, RS and PS at 256/384/512, ES256, ES384 and EdDSA all signed and verified, while ES512, ES256K and Ed25519 are not variants at all. There is no none variant either, so an unsigned token fails while parsing its header with unknown variant none. Validation::new(alg) is the allow-list: an HS256 token checked with Validation::new(ES256) failed with InvalidAlgorithm, and even with both algorithms allowed, an HS256 token keyed with the PEM text of the EC public key failed with InvalidKeyFormat because an EC DecodingKey cannot act as an HMAC secret.

Claim checks are stricter than in most libraries: a token without exp fails with Missing required claim: exp, and the default leeway is 60 seconds — a token that expired 57 seconds earlier still validated. Reduce leeway and set the issuer and audience on the Validation, as the sample does. ES256 refuses a P-384 key at signing time with InvalidEcdsaKey.

ES256 signatures are the 64-byte raw r‖s that JWS requires, and a DER signature fails with InvalidSignature. Keys load from PEM (from_ec_pem, from_rsa_pem, from_ed_pem) and DER; for Ed25519 DER the encoding key took PKCS#8 and the decoding key the raw 32-byte public key. Inspect tokens with the JWT debugger and test HMAC secrets with the JWT secret checker.

For signatures outside JWT, use the backend crates directly: aws-lc-rs or RustCrypto ecdsa.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/jsonwebtoken-rust/run.sh ran jsonwebtoken (Rust) 11.1.0 in the container image rust:1.99.1-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • JWS algorithms

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384EdDSA

    HS, RS and PS at 256/384/512, ES256, ES384 and EdDSA signed and verified; ES512, ES256K and Ed25519 are not Algorithm variants.

    Recorded by the check
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, EdDSA
    evidence jws-algorithms: ES512: not an Algorithm variant
    evidence jws-algorithms: ES256K: not an Algorithm variant
    evidence jws-algorithms: Ed25519: not an Algorithm variant
  • ES256 signature encoding

    raw

    ES256 signatures are 64 bytes of raw r‖s.

    Recorded by the check
    fact jws-ecdsa-encoding = raw
  • DER signature inside a JWS

    rejected

    A DER-encoded ES256 signature fails with InvalidSignature.

    Recorded by the check
    fact jws-der-signature = rejected
    evidence jws-der-signature: Err("InvalidSignature")
  • Algorithm allow-list

    required

    Validation::new(alg) sets the allowed algorithms; an HS256 token checked with Validation::new(ES256) failed with InvalidAlgorithm.

    Recorded by the check
    fact jwt-algorithm-allowlist = required
    evidence jwt-algorithm-allowlist: HS256 token with Validation::new(ES256): Err("InvalidAlgorithm")
  • Unsigned tokens (alg none)

    rejected

    none is not an Algorithm variant: the header fails to parse with unknown variant none.

    Recorded by the check
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: Err("JSON error: unknown variant `none`, expected one of `HS256`, `HS384`, `HS512`, `ES256`, `ES384`, `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `EdDSA` at line 1 column 13"); "none" parses as Algorithm: false
  • Public key used as an HMAC secret

    rejected

    With ES256 and HS256 both allowed, an HS256 token keyed with the PEM text failed against the EC DecodingKey with InvalidKeyFormat.

    Recorded by the check
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: algorithms [ES256, HS256], EC DecodingKey: Err("InvalidKeyFormat")
  • Curve checked against alg

    enforced

    Signing ES256 with a P-384 key fails with InvalidEcdsaKey.

    Recorded by the check
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: Ok(Err("InvalidEcdsaKey"))
  • Default clock skew

    60s

    Validation::new sets leeway to 60 seconds: a token expired 57 seconds earlier validated, one expired 63 seconds earlier did not.

    Recorded by the check
    fact jwt-clock-skew = 60s
    evidence jwt-clock-skew: Validation::new(HS256).leeway = 60
  • Expiry claim required

    yes

    A token without exp fails with Missing required claim: exp.

    Recorded by the check
    fact jwt-exp-required = yes
    evidence jwt-exp-required: Err("Missing required claim: exp")
  • No cryptography backend selected

    runtime-panic

    With neither aws_lc_rs nor rust_crypto enabled, the crate compiled and the first encode() panicked with Could not automatically determine the process-level CryptoProvider from jsonwebtoken crate features.

    Recorded by the check
    fact crypto-provider-without-feature = runtime-panic
    evidence crypto-provider-without-feature: default features compile; the first encode() panics with: Could not automatically determine the process-level CryptoProvider from jsonwebtoken crate features.

Usage

Issue an HS256 token and validate it with the algorithm, issuer and a 30-second leeway set explicitly.
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

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

fn main() {
    // Choose a backend explicitly: features = ["aws_lc_rs"] or ["rust_crypto"].
    let secret = b"use at least 32 random bytes from a secrets manager";
    let exp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 600;
    let claims = Claims { sub: "user-42".into(), iss: "https://auth.example.com".into(), exp };
    let token = encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret)).unwrap();

    // Validation::new(alg) is the allow-list; exp is required, leeway defaults to 60 s.
    let mut validation = Validation::new(Algorithm::HS256);
    validation.set_issuer(&["https://auth.example.com"]);
    validation.leeway = 30;
    let data = decode::<Claims>(&token, &DecodingKey::from_secret(secret), &validation).unwrap();
    println!("sub: {}", data.claims.sub);
}

The snippet harness runs every sample verbatim in the container image rust:1.99.1-trixie, and the run fails unless the output matches.

Command
cargo run --quiet --release
Output must match (regular expression)
sub: user-42

Libraries