jwtiny
Minimal, type-safe JSON Web Token (JWT) validation for Rust.
jwtiny validates JWT tokens through a builder-pattern API that enforces correct validation order at compile time. Born from the need for jsonwebtoken with miniserde support, it evolved into a generic JWT library prioritizing safety, clarity, and zero-cost abstractions.
Overview
JWTs (JSON Web Tokens) encode claims as JSON objects secured by digital signatures or message authentication codes. Validating them requires parsing Base64URL-encoded segments, verifying signatures with cryptographic keys, and checking temporal claims like expiration.
Common pitfalls include algorithm confusion attacks (accepting asymmetric algorithms when only symmetric keys are trusted), server-side request forgery (SSRF) via untrusted issuer URLs, and timing vulnerabilities in signature comparison.
jwtiny addresses these through a type-safe state machine: parsing yields a ParsedToken, issuer validation produces a TrustedToken, signature verification creates a VerifiedToken, and claims validation returns the final Token. Each stage must complete before the next begins, enforced by Rust's type system. The builder pattern configures all steps upfront, then executes them atomically—preventing partial validation and ensuring cryptographic keys are only used after issuer checks complete.
Features
| Feature | Default | Description |
|---|---|---|
| HMAC | ✅ | HMAC algorithms (HS256, HS384, HS512) — always enabled |
rsa |
❌ | RSA algorithms (RS256, RS384, RS512) |
ecdsa |
❌ | ECDSA algorithms (ES256, ES384) |
aws-lc-rs |
❌ | Use aws-lc-rs backend instead of ring for RSA/ECDSA |
all-algorithms |
❌ | Enable all asymmetric algorithms (RSA + ECDSA) |
remote |
❌ | Remote JWKS fetching (requires HTTP client implementation) |
remote-rustls |
❌ | HTTPS support for JWKS (provide HTTPS-capable client) |
Quick Start
Add jwtiny to your Cargo.toml:
[]
= "1.0"
For asymmetric algorithms (RSA, ECDSA), enable features:
= { = "1.0", = ["rsa", "ecdsa"] }
Minimal example validating an HMAC-signed token:
use *;
let token = new
.ensure_issuer
.verify_signature
.validate_token
.run?;
println!;
Examples
HMAC Validation
For tokens signed with symmetric keys (HS256, HS384, HS512):
use *;
let token = new
.skip_issuer_check
.verify_signature
.validate_token
.run?;
RSA Public Key Validation
Requires the rsa feature:
use *;
let token = new
.ensure_issuer
.verify_signature
.validate_token
.run?;
ECDSA Public Key Validation
Requires the ecdsa feature. Supports P-256 and P-384 curves:
use *;
let token = new
.ensure_issuer
.verify_signature
.validate_token
.run?;
JWKS Flow (Remote Key Fetching)
Requires the remote feature. Fetch public keys from a JWKS endpoint:
use *;
use HttpClient;
// Create an HTTP client function pointer
let http_client: HttpClient = ;
// Validate with automatic key resolution from JWKS
let token = new
.ensure_issuer
.verify_signature
.validate_token
.run_async
.await?;
Security note: Always validate the issuer before enabling JWKS fetching. Without issuer validation, an attacker can craft a token with an arbitrary iss claim, causing your application to fetch keys from attacker-controlled URLs—a classic SSRF vulnerability.
API Overview
The validation flow proceeds through distinct stages, each producing a new type:
// Stage 1: Parse the token string
let parsed = from_string?;
// Stage 2: Build the validation pipeline
let token = new
.ensure_issuer // Required: validate issuer (or use .skip_issuer_check())
.verify_signature // Required: verify signature
.validate_token // Optional: defaults to ValidationConfig::default() if omitted
.run?; // Execute all stages atomically
// Stage 3: Access validated claims
token.subject; // Option<&str>
token.issuer; // Option<&str>
token.claims; // &Claims
Issuer Validation
Always validate issuers when using JWKS to prevent SSRF attacks:
// ✅ Correct: Allowlist trusted issuers
.ensure_issuer
// For same-service tokens, explicitly skip
.skip_issuer_check
Signature Verification
Choose verification based on the algorithm family:
HMAC (symmetric keys) — always enabled:
with_secret
.allow_algorithms
RSA (asymmetric keys) — requires rsa feature:
with_key
.allow_algorithms
ECDSA (asymmetric keys) — requires ecdsa feature:
with_key
Algorithm restrictions are recommended to prevent algorithm confusion. Without .allow_algorithms(), any algorithm matching the key type is accepted; with it, only explicitly allowed algorithms pass validation.
Claims Validation
Configure temporal and claim-specific checks:
default
.require_audience // Validate `aud` claim
.max_age // Token must be < 1 hour old
.clock_skew // Allow 60s clock skew
.no_exp_validation // Skip expiration (dangerous)
.custom
Architecture
The library enforces a validation pipeline through type-level state transitions:
ParsedToken (parsed header and payload)
│ .ensure_issuer()
▼
TrustedToken (issuer validated; internal type)
│ .verify_signature()
▼
VerifiedToken (signature verified; internal type)
│ .validate_token()
▼
ValidatedToken (claims validated; internal type)
│ .run() / .run_async()
▼
Token (public API; safe to use)
Only the final Token type is exposed publicly. Intermediate types (TrustedToken, VerifiedToken, ValidatedToken) are internal, preventing partial validation from escaping the builder.
Design Philosophy
jwtiny makes deliberate design choices that distinguish it from other JWT libraries:
Compile-Time Validation Guarantees
The type-level state machine prevents an entire class of security bugs endemic to JWT libraries. Intermediate validation states remain internal to the builder, making partial validation impossible. You cannot accidentally skip signature verification or fetch JWKS keys before validating the issuer—the compiler won't allow it.
This design directly addresses RFC 8725 security requirements at the type level rather than through runtime checks.
Zero-Cost Abstractions
The builder pattern compiles away entirely. Validation logic monomorphizes per algorithm type, producing the same machine code as hand-written validation sequences. There is no runtime overhead for the safety guarantees.
Minimal Binary Footprint
The default HMAC-only build produces a 403KB compiled library artifact. Enabling all algorithms with the ring backend grows this to ~2-3MB—significantly smaller than comparable JWT libraries.
- Feature flags for algorithm families: HMAC support is always enabled (lightweight). RSA and ECDSA are opt-in via features.
- No embedded HTTP client: Remote JWKS fetching uses a function pointer interface. You provide the HTTP implementation (e.g.,
reqwest,ureq) rather than forcing a dependency.
Cryptographic Backend Flexibility
Support for both ring (default) and aws-lc-rs backends allows FIPS compliance when required, without forcing it on users who don't need it. The backend choice is orthogonal to algorithm selection.
Production-Grade Automation
- Bundle size tracking in CI—every feature combination's artifact size is measured and tracked
- Matrix testing across 7 feature combinations and both cryptographic backends
- Docker-based integration tests with real JWKS endpoints (
jwkserve) - Comprehensive benchmarking for performance regression detection
Security
Algorithm Confusion Prevention
Always restrict algorithms explicitly. Without restrictions, a token declaring RS256 might be accepted when you only intended to allow HS256:
// ✅ Correct: Only allow the algorithm you trust
.allow_algorithms
// ❌ Incorrect: Accepts any algorithm compatible with the key type
with_secret // No restrictions
SSRF Prevention
When using JWKS, validate issuers before fetching keys:
// ✅ Correct: Allowlist trusted issuers
.ensure_issuer
// ❌ Incorrect: Attacker can make you fetch from any URL
.skip_issuer_check // Dangerous with JWKS!
"none" Algorithm Rejection
The "none" algorithm (unsigned tokens) is always rejected per RFC 8725:
from_string
// Returns: Error::NoneAlgorithmRejected
Timing Attack Protection
HMAC signature verification uses constant-time comparison via the constant_time_eq crate, preventing timing-based key recovery attacks.
Cryptographic Backends
jwtiny supports two backends for RSA and ECDSA:
ring(default) — battle-tested cryptography libraryaws-lc-rs— FIPS-validated AWS cryptography library
Select exactly one backend. The choice affects signature verification compatibility.
Using ring (default)
[]
= { = "1.0", = ["rsa", "ecdsa"] }
Using aws-lc-rs
[]
= { = "1.0", = ["rsa", "ecdsa", "aws-lc-rs"] }
Compatibility note: If you're verifying tokens signed by services using jsonwebtoken with the aws_lc_rs feature (e.g., jwkserve), use the aws-lc-rs feature to ensure compatibility.
Testing
jwtiny includes comprehensive test coverage across algorithm families, edge cases, and integration scenarios.
Running Tests
# All features with default backend
# Specific algorithm features
# aws-lc-rs backend (for compatibility testing)
# Remote JWKS fetching
# Run specific test suite
Test Coverage
- Algorithm tests (
tests/algorithm_round_trips.rs): Round-trip signing and verification for HMAC, RSA, and ECDSA - Integration tests (
tests/jwkserve_integration.rs): End-to-end RS256 verification via JWKS (requires Docker) - Edge cases (
tests/edge_cases.rs): Token format validation, Base64URL edge cases, claims validation, algorithm confusion prevention - JWK support (
tests/jwk_support.rs): JWK metadata handling, key selection, RSA/ECDSA key extraction - JWT.io compatibility (
tests/jwtio_compatibility.rs): Verification of canonical JWT.io example tokens - Custom headers (
tests/custom_headers.rs): Header field preservation (kid,typ, custom fields), field order invariance, real-world header formats - Key formats (
tests/key_formats.rs): PKCS#8 DER, PKCS#1 DER, PEM format conversion, invalid/truncated key handling
Running Examples
License
MIT