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