jwt_compact/lib.rs
1//! Minimalistic [JSON web token (JWT)][JWT] implementation with focus on type safety
2//! and secure cryptographic primitives.
3//!
4//! # Design choices
5//!
6//! - JWT signature algorithms (i.e., cryptographic algorithms providing JWT integrity)
7//! are expressed via the [`Algorithm`] trait, which uses fully typed keys and signatures.
8//! - [JWT header] is represented by the [`Header`] struct. Notably, `Header` does not
9//! expose the [`alg` field].
10//! Instead, `alg` is filled automatically during token creation, and is compared to the
11//! expected value during verification. (If you do not know the JWT signature algorithm during
12//! verification, you're doing something wrong.) This eliminates the possibility
13//! of [algorithm switching attacks][switching].
14//!
15//! # Additional features
16//!
17//! - The crate supports more compact [CBOR] encoding of the claims. This feature is enabled
18//! via the [`ciborium` feature](#cbor-support).
19//! - The crate supports `EdDSA` algorithm with the Ed25519 elliptic curve, and `ES256K` algorithm
20//! with the secp256k1 elliptic curve.
21//! - Supports basic [JSON Web Key](https://tools.ietf.org/html/rfc7517.html) functionality,
22//! e.g., for converting keys to / from JSON or computing
23//! [a key thumbprint](https://tools.ietf.org/html/rfc7638).
24//!
25//! ## Supported algorithms
26//!
27//! | Algorithm(s) | Feature | Description |
28//! |--------------|---------|-------------|
29//! | `HS256`, `HS384`, `HS512` | - | Uses pure Rust [`sha2`] crate |
30//! | `EdDSA` (Ed25519) | [`exonum-crypto`] | [`libsodium`] binding |
31//! | `EdDSA` (Ed25519) | [`ed25519-dalek`] | Pure Rust implementation |
32//! | `EdDSA` (Ed25519) | [`ed25519-compact`] | Compact pure Rust implementation, WASM-compatible |
33//! | `ES256K` | `es256k` | [Rust binding][`secp256k1`] for [`libsecp256k1`] |
34//! | `ES256K` | [`k256`] | Pure Rust implementation |
35//! | `ES256` | [`p256`] | Pure Rust implementation |
36//! | `RS*`, `PS*` (RSA) | `rsa` | Uses pure Rust [`rsa`] crate with blinding |
37//!
38//! `EdDSA` and `ES256K` algorithms are somewhat less frequently supported by JWT implementations
39//! than others since they are recent additions to the JSON Web Algorithms (JWA) suit.
40//! They both work with elliptic curves
41//! (Curve25519 and secp256k1; both are widely used in crypto community and believed to be
42//! securely generated). These algs have 128-bit security, making them an alternative
43//! to `ES256`.
44//!
45//! RSA support requires a system-wide RNG retrieved via the [`getrandom`] crate.
46//! In case of a compilation failure in the `getrandom` crate, you may want
47//! to include it as a direct dependency and specify one of its features
48//! to assist `getrandom` with choosing an appropriate RNG implementation; consult `getrandom` docs
49//! for more details. See also WASM and bare-metal E2E tests included
50//! in the [source code repository] of this crate.
51//!
52//! ## CBOR support
53//!
54//! If the `ciborium` crate feature is enabled (and it is enabled by default), token claims can
55//! be encoded using [CBOR] with the [`AlgorithmExt::compact_token()`] method.
56//! The compactly encoded JWTs have the [`cty` field] (content type) in their header
57//! set to `"CBOR"`. Tokens with such encoding can be verified in the same way as ordinary tokens;
58//! see [examples below](#examples).
59//!
60//! If the `ciborium` feature is disabled, `AlgorithmExt::compact_token()` is not available.
61//! Verifying CBOR-encoded tokens in this case is not supported either;
62//! a [`ParseError::UnsupportedContentType`] will be returned when creating an [`UntrustedToken`]
63//! from the token string.
64//!
65//! # `no_std` support
66//!
67//! The crate supports a `no_std` compilation mode. This is controlled by two features:
68//! `clock` and `std`; both are on by default.
69//!
70//! - The `clock` feature enables getting the current time using `Utc::now()` from [`chrono`].
71//! Without it, some [`TimeOptions`] constructors, such as the `Default` impl,
72//! are not available. It is still possible to create `TimeOptions` with an explicitly specified
73//! clock function, or to set / verify time-related [`Claims`] fields manually.
74//! - The `std` feature is propagated to the core dependencies and enables `std`-specific
75//! functionality (such as error types implementing the standard `Error` trait).
76//!
77//! Some `alloc` types are still used in the `no_std` mode, such as `String`, `Vec` and `Cow`.
78//!
79//! Note that not all crypto backends are `no_std`-compatible.
80//!
81//! [JWT]: https://jwt.io/
82//! [switching]: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
83//! [JWT header]: https://tools.ietf.org/html/rfc7519#section-5
84//! [`alg` field]: https://tools.ietf.org/html/rfc7515#section-4.1.1
85//! [`cty` field]: https://tools.ietf.org/html/rfc7515#section-4.1.10
86//! [CBOR]: https://tools.ietf.org/html/rfc7049
87//! [`sha2`]: https://docs.rs/sha2/
88//! [`libsodium`]: https://download.libsodium.org/doc/
89//! [`exonum-crypto`]: https://docs.rs/exonum-crypto/
90//! [`ed25519-dalek`]: https://doc.dalek.rs/ed25519_dalek/
91//! [`ed25519-compact`]: https://crates.io/crates/ed25519-compact
92//! [`secp256k1`]: https://docs.rs/secp256k1/
93//! [`libsecp256k1`]: https://github.com/bitcoin-core/secp256k1
94//! [`k256`]: https://docs.rs/k256/
95//! [`p256`]: https://docs.rs/p256/
96//! [`rsa`]: https://docs.rs/rsa/
97//! [`chrono`]: https://docs.rs/chrono/
98//! [`getrandom`]: https://docs.rs/getrandom/
99//! [source code repository]: https://github.com/slowli/jwt-compact
100//!
101//! # Examples
102//!
103//! Basic JWT lifecycle:
104//!
105//! ```
106//! use chrono::{Duration, Utc};
107//! use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
108//! use serde::{Serialize, Deserialize};
109//!
110//! /// Custom claims encoded in the token.
111//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
112//! struct CustomClaims {
113//! /// `sub` is a standard claim which denotes claim subject:
114//! /// https://tools.ietf.org/html/rfc7519#section-4.1.2
115//! #[serde(rename = "sub")]
116//! subject: String,
117//! }
118//!
119//! # fn main() -> anyhow::Result<()> {
120//! // Choose time-related options for token creation / validation.
121//! let time_options = TimeOptions::default();
122//! // Create a symmetric HMAC key, which will be used both to create and verify tokens.
123//! let key = Hs256Key::new(b"super_secret_key_donut_steel");
124//! // Create a token.
125//! let header = Header::empty().with_key_id("my-key");
126//! let claims = Claims::new(CustomClaims { subject: "alice".to_owned() })
127//! .set_duration_and_issuance(&time_options, Duration::days(7))
128//! .set_not_before(Utc::now() - Duration::hours(1));
129//! let token_string = Hs256.token(&header, &claims, &key)?;
130//! println!("token: {token_string}");
131//!
132//! // Parse the token.
133//! let token = UntrustedToken::new(&token_string)?;
134//! // Before verifying the token, we might find the key which has signed the token
135//! // using the `Header.key_id` field.
136//! assert_eq!(token.header().key_id, Some("my-key".to_owned()));
137//! // Validate the token integrity.
138//! let token: Token<CustomClaims> = Hs256.validator(&key).validate(&token)?;
139//! // Validate additional conditions.
140//! token.claims()
141//! .validate_expiration(&time_options)?
142//! .validate_maturity(&time_options)?;
143//! // Now, we can extract information from the token (e.g., its subject).
144//! let subject = &token.claims().custom.subject;
145//! assert_eq!(subject, "alice");
146//! # Ok(())
147//! # } // end main()
148//! ```
149//!
150//! ## Compact JWT
151//!
152//! ```
153//! # use chrono::Duration;
154//! # use hex_buffer_serde::{Hex as _, HexForm};
155//! # use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
156//! # use serde::{Serialize, Deserialize};
157//! /// Custom claims encoded in the token.
158//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
159//! struct CustomClaims {
160//! /// `sub` is a standard claim which denotes claim subject:
161//! /// https://tools.ietf.org/html/rfc7519#section-4.1.2
162//! /// The custom serializer we use allows to efficiently
163//! /// encode the subject in CBOR.
164//! #[serde(rename = "sub", with = "HexForm")]
165//! subject: [u8; 32],
166//! }
167//!
168//! # fn main() -> anyhow::Result<()> {
169//! let time_options = TimeOptions::default();
170//! let key = Hs256Key::new(b"super_secret_key_donut_steel");
171//! let claims = Claims::new(CustomClaims { subject: [111; 32] })
172//! .set_duration_and_issuance(&time_options, Duration::days(7));
173//! let token = Hs256.token(&Header::empty(), &claims, &key)?;
174//! println!("token: {token}");
175//! let compact_token = Hs256.compact_token(&Header::empty(), &claims, &key)?;
176//! println!("compact token: {compact_token}");
177//! // The compact token should be ~40 chars shorter.
178//!
179//! // Parse the compact token.
180//! let token = UntrustedToken::new(&compact_token)?;
181//! let token: Token<CustomClaims> = Hs256.validator(&key).validate(&token)?;
182//! token.claims().validate_expiration(&time_options)?;
183//! // Now, we can extract information from the token (e.g., its subject).
184//! assert_eq!(token.claims().custom.subject, [111; 32]);
185//! # Ok(())
186//! # } // end main()
187//! ```
188//!
189//! ## JWT with custom header fields
190//!
191//! ```
192//! # use chrono::Duration;
193//! # use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
194//! # use serde::{Deserialize, Serialize};
195//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
196//! struct CustomClaims { subject: [u8; 32] }
197//!
198//! /// Additional fields in the token header.
199//! #[derive(Debug, Clone, Serialize, Deserialize)]
200//! struct HeaderExtensions { custom: bool }
201//!
202//! # fn main() -> anyhow::Result<()> {
203//! let time_options = TimeOptions::default();
204//! let key = Hs256Key::new(b"super_secret_key_donut_steel");
205//! let claims = Claims::new(CustomClaims { subject: [111; 32] })
206//! .set_duration_and_issuance(&time_options, Duration::days(7));
207//! let header = Header::new(HeaderExtensions { custom: true })
208//! .with_key_id("my-key");
209//! let token = Hs256.token(&header, &claims, &key)?;
210//! print!("token: {token}");
211//!
212//! // Parse the token.
213//! let token: UntrustedToken<HeaderExtensions> =
214//! token.as_str().try_into()?;
215//! // Token header (incl. custom fields) can be accessed right away.
216//! assert_eq!(token.header().key_id.as_deref(), Some("my-key"));
217//! assert!(token.header().other_fields.custom);
218//! // Token can then be validated as usual.
219//! let token = Hs256.validator::<CustomClaims>(&key).validate(&token)?;
220//! assert_eq!(token.claims().custom.subject, [111; 32]);
221//! # Ok(())
222//! # } // end main()
223//! ```
224
225#![cfg_attr(not(feature = "std"), no_std)]
226// Documentation settings.
227#![cfg_attr(docsrs, feature(doc_cfg))]
228#![doc(html_root_url = "https://docs.rs/jwt-compact/0.8.0")]
229// Linter settings.
230#![warn(missing_debug_implementations, missing_docs, bare_trait_objects)]
231#![warn(clippy::all, clippy::pedantic)]
232#![allow(
233 clippy::missing_errors_doc,
234 clippy::must_use_candidate,
235 clippy::module_name_repetitions
236)]
237
238pub mod alg;
239mod claims;
240mod error;
241pub mod jwk;
242mod token;
243mod traits;
244
245// Polyfill for `alloc` types.
246mod alloc {
247 #[cfg(not(feature = "std"))]
248 extern crate alloc as std;
249
250 pub use std::{
251 borrow::{Cow, ToOwned},
252 boxed::Box,
253 format,
254 string::{String, ToString},
255 vec::Vec,
256 };
257}
258
259/// Prelude to neatly import all necessary stuff from the crate.
260pub mod prelude {
261 #[doc(no_inline)]
262 pub use crate::{AlgorithmExt as _, Claims, Header, TimeOptions, Token, UntrustedToken};
263}
264
265pub use crate::{
266 claims::{Claims, Empty, TimeOptions},
267 error::{Claim, CreationError, ParseError, ValidationError},
268 token::{Header, SignedToken, Thumbprint, Token, UntrustedToken},
269 traits::{Algorithm, AlgorithmExt, AlgorithmSignature, Renamed, Validator},
270};
271
272#[cfg(doctest)]
273doc_comment::doctest!("../README.md");