Skip to main content

Crate jwt_simple

Crate jwt_simple 

Source
Expand description

GitHub CI Docs.rs crates.io

§JWT-Simple

A new JWT (JSON Web Tokens) implementation for Rust that focuses on simplicity, while avoiding common JWT security pitfalls.

jwt-simple is unopinionated and supports all commonly deployed authentication and signature algorithms:

JWT algorithm nameDescription
HS256HMAC-SHA-256
HS384HMAC-SHA-384
HS512HMAC-SHA-512
BLAKE2BBLAKE2B-256
RS256RSA with PKCS#1v1.5 padding / SHA-256
RS384RSA with PKCS#1v1.5 padding / SHA-384
RS512RSA with PKCS#1v1.5 padding / SHA-512
PS256RSA with PSS padding / SHA-256
PS384RSA with PSS padding / SHA-384
PS512RSA with PSS padding / SHA-512
ES256ECDSA over p256 / SHA-256
ES384ECDSA over p384 / SHA-384
ES256KECDSA over secp256k1 / SHA-256
EdDSAEd25519
ML-DSA-44ML-DSA-44 (FIPS 204, post-quantum)
ML-DSA-65ML-DSA-65 (FIPS 204, post-quantum)
ML-DSA-87ML-DSA-87 (FIPS 204, post-quantum)

JWE (JSON Web Encryption) is also supported with the following key management algorithms:

JWE algorithm nameDescription
RSA-OAEPRSA with OAEP using SHA-1 (not recommended)
A256KWAES-256 Key Wrap
A128KWAES-128 Key Wrap
ECDH-ES+A256KWECDH (P-256 or X25519) with AES-256 Key Wrap
ECDH-ES+A128KWECDH (P-256 or X25519) with AES-128 Key Wrap

Content encryption uses AES-GCM (A256GCM or A128GCM).

jwt-simple can be compiled out of the box to WebAssembly/WASI. It is fully compatible with Fastly Compute service.

Important: JWT’s purpose is to verify that data has been created by a party knowing a secret key. It does not provide any kind of confidentiality: JWT data is simply encoded as Base64, and is not encrypted.

§Usage

cargo.toml:

[dependencies]
jwt-simple = "0.13"

Rust:

use jwt_simple::prelude::*;

Errors are returned as jwt_simple::Error values (alias for the Error type of the anyhow crate).

§Authentication (symmetric, HS* JWT algorithms) example

Authentication schemes use the same key for creating and verifying tokens. In other words, both parties need to ultimately trust each other, or else the verifier could also create arbitrary tokens.

§Keys and tokens creation

Key creation:

use jwt_simple::prelude::*;

// create a new key for the `HS256` JWT algorithm
let key = HS256Key::generate();

A key can be exported as bytes with key.to_bytes(), and restored with HS256Key::from_bytes().

Token creation:

ⓘ
// create claims valid for 2 hours
let claims = Claims::create(Duration::from_hours(2));
let token = key.authenticate(claims)?;

-> Done!

§Token verification

ⓘ
let claims = key.verify_token::<NoCustomClaims>(&token, None)?;

-> Done! No additional steps required.

Key expiration, start time, authentication tags, etc. are automatically verified. The function fails with JWTError::InvalidAuthenticationTag if the authentication tag is invalid for the given key.

The full set of claims can be inspected in the claims object if necessary. NoCustomClaims means that only the standard set of claims is used by the application, but application-defined claims can also be supported.

Extra verification steps can optionally be enabled via the VerificationOptions structure:

ⓘ
let mut options = VerificationOptions::default();
// Accept tokens that will only be valid in the future
options.accept_future = true;
// Accept tokens even if they have expired up to 15 minutes after the deadline,
// and/or they will be valid within 15 minutes.
// Note that 15 minutes is the default, since it is very common for clocks to be slightly off.
options.time_tolerance = Some(Duration::from_mins(15));
// Reject tokens if they were issued more than 1 hour ago
options.max_validity = Some(Duration::from_hours(1));
// Reject tokens if they don't include an issuer from that set
options.allowed_issuers = Some(HashSet::from_strings(&["example app"]));

// see the documentation for the full list of available options

let claims = key.verify_token::<NoCustomClaims>(&token, Some(options))?;

Note that allowed_issuers and allowed_audiences are not strings, but sets of strings (using the HashSet type from the Rust standard library), as the application can allow multiple values.

§Signatures (asymmetric, RS*, PS*, ES*, EdDSA and ML-DSA algorithms) example

A signature requires a key pair: a secret key used to create tokens, and a public key, that can only verify them.

Always use a signature scheme if both parties do not ultimately trust each other, such as tokens exchanged between clients and API providers.

§Key pairs and tokens creation

Key creation:

§ES256
use jwt_simple::prelude::*;

// create a new key pair for the `ES256` JWT algorithm
let key_pair = ES256KeyPair::generate();

// a public key can be extracted from a key pair:
let public_key = key_pair.public_key();
§ES384
use jwt_simple::prelude::*;

// create a new key pair for the `ES384` JWT algorithm
let key_pair = ES384KeyPair::generate();

// a public key can be extracted from a key pair:
let public_key = key_pair.public_key();
§ML-DSA

ML-DSA is a post-quantum signature scheme (FIPS 204). Signing and verification are fast, but public keys and signatures are much larger than their classical counterparts.

use jwt_simple::prelude::*;

// create a new key pair for the `ML-DSA-44` JWT algorithm
let key_pair = MLDSA44KeyPair::generate();

// a public key can be extracted from a key pair:
let public_key = key_pair.public_key();

MLDSA65KeyPair and MLDSA87KeyPair are also available, for the ML-DSA-65 and ML-DSA-87 algorithms. ML-DSA-44 is the recommended choice: its security level is good enough for all practical purposes, and it is much faster than the other variants. A key pair is serialized as its 32-byte seed (to_bytes()/from_bytes()), and a public key as its raw byte representation.

Keys can be exported as bytes for later reuse, and imported from bytes or, for RSA, from individual parameters, DER-encoded data or PEM-encoded data. Every asymmetric key can also be imported and exported as a JSON Web Key.

RSA key pair creation, using OpenSSL and PEM importation of the secret key:

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
ⓘ
let key_pair = RS384KeyPair::from_pem(private_pem_file_content)?;
let public_key = RS384PublicKey::from_pem(public_pem_file_content)?;

RSA keys must have a modulus of 2048 to 4096 bits, and a public exponent of 65537. This applies to every format, and to RSA-OAEP keys as well. There is no option to accept keys outside these bounds.

Smaller moduli fail with JWTError::WeakKey, and other exponents with JWTError::InvalidPublicKey or JWTError::InvalidKeyPair. Larger moduli fail with JWTError::UnsupportedRSAModulus, except DER and PEM public keys with the pure-Rust backend, whose parser rejects them with its own error.

A few providers still publish weaker keys. For example, the Yahoo key set only served 1024-bit RSA keys when it was checked on 2026-09-25. Loading these keys fails with WeakKey, so tokens from Sign in with Yahoo can’t be verified.

Key sets rotate, so check the keys that are currently published rather than relying on this example.

Ed25519 public keys that are not canonically encoded, or that are points of small order, are rejected by every constructor. Ed25519 key pairs imported from raw bytes must contain the public key that their seed derives.

Token creation and verification work the same way as with HS* algorithms, except that tokens are created with a key pair, and verified using the corresponding public key.

Token creation:

ⓘ
// create claims valid for 2 hours
let claims = Claims::create(Duration::from_hours(2));
let token = key_pair.sign(claims)?;

Token verification:

ⓘ
let claims = public_key.verify_token::<NoCustomClaims>(&token, None)?;

Available verification options are identical to the ones used with symmetric algorithms.

§JWE (Encrypted tokens)

While JWT signatures provide authenticity (verifying who created the token), JWE provides confidentiality by encrypting the token content. Use JWE when the claims contain sensitive data that should not be visible to intermediaries.

§RSA-OAEP key management

RSA-OAEP uses asymmetric encryption: anyone with the public key can encrypt tokens, but only the private key holder can decrypt them.

ⓘ
use jwt_simple::prelude::*;

// Generate a key pair (2048 bits minimum, 4096 recommended for high security)
let decryption_key = RsaOaepDecryptionKey::generate(2048)?;
let encryption_key = decryption_key.encryption_key();

// Encrypt a token
let claims = Claims::create(Duration::from_hours(1))
.with_subject("user@example.com");
let token = encryption_key.encrypt(claims)?;

// Decrypt the token
let claims: JWTClaims<NoCustomClaims> = decryption_key.decrypt_token(&token, None)?;

Keys can be exported and imported using PEM or DER formats, similar to RSA signature keys.

RSA-OAEP uses SHA-1 inside OAEP, so tokens work with other JOSE implementations.

§AES Key Wrap

For symmetric encryption where the same key is used for both encryption and decryption:

ⓘ
use jwt_simple::prelude::*;

// Generate a 256-bit key
let key = A256KWKey::generate();

// Or create from existing bytes
let key = A256KWKey::from_bytes(&raw_key_bytes)?;

// Encrypt
let claims = Claims::create(Duration::from_hours(1));
let token = key.encrypt(claims)?;

// Decrypt
let claims: JWTClaims<NoCustomClaims> = key.decrypt_token(&token, None)?;

A128KWKey is also available for 128-bit keys.

§ECDH-ES key agreement

ECDH-ES uses elliptic curve Diffie-Hellman for key agreement. Like RSA-OAEP, it uses asymmetric keys but is more efficient:

ⓘ
use jwt_simple::prelude::*;

// Generate a key pair
let decryption_key = EcdhEsA256KWDecryptionKey::generate();
let encryption_key = decryption_key.encryption_key();

// Encrypt
let claims = Claims::create(Duration::from_hours(1));
let token = encryption_key.encrypt(claims)?;

// Decrypt
let claims: JWTClaims<NoCustomClaims> = decryption_key.decrypt_token(&token, None)?;

When a received token includes apu and apv, they are used to derive the key. They must be valid base64url, and different from each other when both are present. Tokens created by jwt-simple don’t include them.

§ECDH-ES key agreement with X25519

The same key agreement modes work with X25519 keys, which are smaller and faster than P-256 keys:

ⓘ
use jwt_simple::prelude::*;

let decryption_key = X25519EcdhEsA256KWDecryptionKey::generate();
let encryption_key = decryption_key.encryption_key();

let claims = Claims::create(Duration::from_hours(1));
let token = encryption_key.encrypt(claims)?;

let claims: JWTClaims<NoCustomClaims> = decryption_key.decrypt_token(&token, None)?;

X25519EcdhEsA128KWDecryptionKey and X25519EcdhEsA128KWEncryptionKey implement ECDH-ES+A128KW. Keys can be imported and exported as raw bytes, DER, PEM or JWK.

Public keys must be canonically encoded (top bit clear, value below 2^255 - 19), and points of small order are rejected. The same checks apply to the ephemeral key of every received token.

JWE tokens support the same claim types and custom claims as JWT signatures. Decryption options allow validating claims, requiring specific key IDs, and limiting token size.

§JSON Web Keys (JWK)

Every asymmetric key type supports JSON Web Key import and export, and can compute its JWK thumbprint:

use jwt_simple::prelude::*;

let key_pair = ES256KeyPair::generate().with_key_id("2026-09");

// Publish the public key, and load it on the verifier side.
let public_jwk = key_pair.public_key().to_jwk();
let public_key = ES256PublicKey::from_jwk(&public_jwk)?;
let token = key_pair.sign(Claims::create(Duration::from_hours(1)))?;
public_key.verify_token::<NoCustomClaims>(&token, None)?;

// Keep this private.
let private_jwk = key_pair.to_jwk();
let key_pair = ES256KeyPair::from_jwk(&private_jwk)?;
assert_eq!(key_pair.jwk_thumbprint(), public_key.jwk_thumbprint());

The same from_jwk(), to_jwk() and jwk_thumbprint() methods exist on the Ed25519, ES256, ES384, ES256K, RS*, PS* and ML-DSA key types, and on the RSA-OAEP, ECDH-ES and X25519 JWE keys. Symmetric keys have no JWK support.

to_jwk() on a key pair or a decryption key exports the private key. Treat its output as the secret it is, and use public_key().to_jwk() or encryption_key().to_jwk() to share a key.

jwt-simple wipes its own temporary copies of private keys when it can, but the application is responsible for the strings and bytes it gets back.

A JWK is only accepted by the key type it describes. There is no generic key type that picks an algorithm from the alg member of a key. An application that receives a key set picks the entry it trusts, usually by kid, and passes its JSON to the key type it expects.

Exported keys include alg, use (sig for signature keys, enc for encryption keys) and, if one is set, the key identifier as kid. This keeps other implementations from using a key for something else.

Ed25519 keys are exported with alg set to EdDSA, the name deployed verifiers expect. Ed25519 is accepted on import as well, and Ed25519 public keys verify tokens using either name.

§Strict JWK validation

Imports are strict, and there is no way to relax them:

  • Documents larger than 8192 bytes (MAX_JWK_LENGTH) are rejected before being parsed, whitespace and unknown members included. A complete 4096-bit RSA private key takes about 3.2 KB.

  • A document must be a single JSON object without duplicate member names. Known members must have the right type, and are never allowed to be null.

  • Key material must be canonical, unpadded base64url, with the exact size the key type requires.

  • Members that belong to other key types are rejected, and a public key can’t include any private member, not even an empty one.

  • A private key must be complete, and its public members must be the ones its private part derives.

  • alg, use and key_ops are optional, but must match the key type when present. key_ops can only list operations the key performs, private keys must list at least one, and signing keys must include sign. This metadata can only narrow what a key is used for. It can’t turn an ES256 key into an ECDH key, or an RS256 key into a PS256 or RSA-OAEP key.

  • A kid becomes the key identifier. Other members, such as ext or x5c, are ignored.

  • Keys go through the same checks as in other formats. Invalid or small-order Ed25519 and X25519 points, EC points that are not on the curve, and RSA keys outside the size and exponent policy are rejected.

Key typeskty, crvPublic membersPrivate members
Ed25519OKP, Ed25519xd, the 32-byte seed
X25519OKP, X25519xd
ES256, ES384, ES256KEC, P-256/P-384/secp256k1x, yd
ECDH-ESEC, P-256x, yd
RS*, PS*, RSA-OAEPRSAn, ed, p, q, dp, dq, qi
ML-DSAAKP (alg is required)pubpriv, the 32-byte seed

EC coordinates keep their leading zeros, and each coordinate is checked on its own.

Java exports of RSA keys sometimes put a zero byte in front of the modulus when its top bit is set. Exactly one such byte is accepted in n, and dropped. The other RSA values must be minimally encoded, and multi-prime keys (oth) are not supported.

§JWK thumbprints

jwk_thumbprint() returns the JWK thumbprint of a key: the SHA-256 hash of its required public members, base64url-encoded. A key pair and its public key have the same thumbprint, which doesn’t depend on kid or any other metadata.

The thumbprint of an ML-DSA key also covers its algorithm.

A thumbprint makes a good key identifier:

ⓘ
let thumbprint = key_pair.jwk_thumbprint();
let key_pair = key_pair.with_key_id(&thumbprint);

create_key_id() keeps its original definition, a hash of the raw public key, so that existing identifiers don’t change.

The sha1_thumbprint() and sha256_thumbprint() methods of Ed25519 and RSA public keys hash their DER encoding. They are not JWK thumbprints.

§Advanced usage

§Custom claims

Claim objects support all the standard claims by default, and they can be set directly or via convenient helpers:

ⓘ
let claims = Claims::create(Duration::from_hours(2)).
with_issuer("Example issuer").with_subject("Example subject");

But application-defined claims can also be used. These simply have to be present in a serializable type (this requires the serde crate):

ⓘ
#[derive(Serialize, Deserialize)]
struct MyAdditionalData {
user_is_admin: bool,
user_country: String,
}
let my_additional_data = MyAdditionalData {
user_is_admin: false,
user_country: "FR".to_string(),
};

Claim creation with custom data:

ⓘ
let claims = Claims::with_custom_claims(my_additional_data, Duration::from_secs(30));

Claim verification with custom data. Note the presence of the custom data type:

ⓘ
let claims = public_key.verify_token::<MyAdditionalData>(&token, None)?;
let user_is_admin = claims.custom.user_is_admin;

§Peeking at metadata before verification

Properties such as the key identifier can be useful prior to tag or signature verification in order to pick the right key out of a set.

ⓘ
let metadata = Token::decode_metadata(&token)?;
let key_id = metadata.key_id();
let algorithm = metadata.algorithm();
// all other standard properties are also accessible

IMPORTANT: neither the key ID nor the algorithm can be trusted. This is an unfixable design flaw of the JWT standard.

As a result, algorithm should be used only for debugging purposes, and never to select a key type. Similarly, key_id should be used only to select a key in a set of keys made for the same algorithm.

At the bare minimum, verification using HS* must be prohibited if a signature scheme was originally used to create the token.

§Creating and attaching key identifiers

Key identifiers indicate to verifiers what public key (or shared key) should be used for verification. They can be attached at any time to existing shared keys, key pairs and public keys:

ⓘ
let public_key_with_id = public_key.with_key_id(&"unique key identifier");

Instead of delegating this to applications, jwt-simple can also create such an identifier for an existing key:

ⓘ
let key_id = public_key.create_key_id();

This creates a text-encoded identifier for the key, attaches it, and returns it.

For a standard identifier that other implementations can compute too, use the key’s JWK thumbprint.

If an identifier has been attached to a shared key or a key pair, tokens created with them will include it.

§Mitigations against replay attacks

jwt-simple includes mechanisms to mitigate replay attacks:

  • Nonces can be created and attached to new tokens using the create_nonce() claim function. The verification procedure can later reject any token that doesn’t include the expected nonce (required_nonce verification option).
  • The verification procedure can reject tokens created too long ago, no matter what their expiration date is. This prevents tokens from malicious (or compromised) signers from being used for too long.
  • The verification procedure can reject tokens created before a date. For a given user, the date of the last successful authentication can be stored in a database, and used later along with this option to reject older (replayed) tokens.

§Salted keys

Symmetric keys, such as the ones used with the HS256, HS384, HS512 and BLAKE2B algorithms, are simple and fast, but have a major downside: signature and verification use the exact same key. Therefore, an adversary having access to the verifier key can forge arbitrary, valid tokens.

Salted keys mitigate this issue in the following way:

  • A random signer salt is created and attached to the shared key. This salt is meant to be known only by the signer.
  • Another salt is computed from the signer salt and is meant to be used for verification.
  • The verifier salt is used to verify the signer salt, which is included in tokens in the salt JWT header.

If the verifier has access to tokens, it can forge arbitrary tokens. But given only the verification code and keys, this is impossible. This greatly improves the security of symmetric keys used for verification on 3rd party servers, such as CDNs.

A salt binds to a key, and can be of any length. The generate_with_salt() function generates both a random symmetric key, and a 32-byte salt.

Example usage:

ⓘ
// Create a random key and a signer salt
let key = HS256Key::generate_with_salt();
let claims = Claims::create(Duration::from_secs(86400));
let token = key.authenticate(claims).unwrap();

A salt is a Salt enum, because it can be either a salt for signing, or a salt for verification. It can be saved and restored:

ⓘ
// Get the salt
let salt = key.salt();
// Attach an existing salt to a key
key.attach_salt(salt)?;

Given a signer salt, the corresponding verifier salt can be computed:

ⓘ
// Compute the verifier salt, given a signer salt
let verifier_salt = key.verifier_salt()?;

The verifier salt doesn’t have to be secret, and can even be hard-coded in the verification code.

Verification:

ⓘ
let verifier_salt = Salt::Verifier(verifier_salt_bytes);
key.attach_salt(verifier_salt)?;
let claims = key.verify_token::<NoCustomClaims>(&token, None)?;

§CWT (CBOR) support

The development code includes a cwt cargo feature that enables experimental parsing and validation of CWT tokens.

Please note that CWT doesn’t support custom claims. The required identifiers haven’t been standardized yet.

Also, the existing Rust crates for JSON and CBOR deserialization are not safe. An untrusted party can send a serialized object that requires a lot of memory and CPU to deserialize. Band-aids have been added for JSON, but with the current Rust tooling, it would be tricky to implement for CBOR.

As a mitigation, we highly recommend rejecting tokens that would be too large in the context of your application. That can be done with the max_token_length verification option.

§Specifying header options

It is possible to change the content type (cty) and signature type (typ) fields of a signed JWT by using the sign_with_options/authenticate_with_options functions and passing in a HeaderOptions struct:

ⓘ
let options = HeaderOptions {
content_type: Some("foo".into()),
signature_type: Some("foo+JWT".into()),
..Default::default()
};
key_pair.sign_with_options(claims, &options).unwrap();

By default, generated JWTs will have a signature type field containing the string “JWT”, and the content type field will not be present.

§Validating content and signature types

By default, jwt_simple ignores the content_type field when doing validation, and checks signature_type to ensure it is either exactly JWT or ends in +JWT, case insensitive, if it is present. Both fields may instead be case-insensitively compared against an expected string:

ⓘ
options.required_signature_type = Some("JWT".into());
options.required_content_type = Some("foo+jwt".into());

When validating CWTs, note that CWTs do not have a content_type field in their header, and therefore attempting to match a specific one by setting required_content_type during validation will always result in an error.

§Working around compilation issues with the boring crate

As a temporary workaround for portability issues with one of the dependencies (the boring crate), this library can be compiled to use only Rust implementations.

In order to do so, import the crate with default-features = false, features = ["pure-rust"] in your Cargo configuration.

Do not do it unconditionally. This is only required for very specific setups and targets, and only until issues with the boring crate have been solved. The way to configure this in Cargo may also change in future versions.

Static builds targeting the musl library don’t require that workaround. Just use cargo-zigbuild to build your project.

§Usage in Web browsers

The wasm32-freestanding target (still sometimes called wasm32-unknown-unknown in Rust) is supported (as in “it compiles”).

However, using a native JavaScript implementation is highly recommended instead. There are high-quality JWT implementations in JavaScript, leveraging the WebCrypto API, that provide better performance and security guarantees than a WebAssembly module.

§Why yet another JWT crate

This crate is not an endorsement of JWT. JWT is an awful design, and one of the many examples that “but this is a standard” doesn’t necessarily mean that it is good.

I would highly recommend PASETO or Biscuit instead if you control both token creation and verification.

However, JWT is still widely used in the industry, and remains absolutely mandatory to communicate with popular APIs.

This crate was designed to:

  • Be simple to use, even for people who are new to Rust
  • Avoid common JWT API pitfalls
  • Support features widely in use. I’d love to limit the algorithm choices to Ed25519, but other methods are required to connect to existing APIs, so we provide them (with the exception of the None signature method for obvious reasons).
  • Minimize code complexity and external dependencies
  • Automatically perform common tasks to prevent misuse. Signature verification and claims validation happen automatically instead of relying on applications.
  • Still allow power users to access everything JWT tokens include if they really need to
  • Work out of the box in a WebAssembly environment, so that it can be used in function-as-a-service platforms.

Modules§

algorithms
claims
common
jwe_header
jwe_token
JWE token building and parsing.
prelude
reexports
token

Structs§

Error
The Error type, a wrapper around a dynamic error type.

Enums§

JWTError