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 two features:
72//! `clock` and `std`; both are 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//! - The `std` feature is propagated to the core dependencies and enables `std`-specific
79//! functionality (such as error types implementing the standard `Error` trait).
80//!
81//! Some `alloc` types are still used in the `no_std` mode, such as `String`, `Vec` and `Cow`.
82//!
83//! Note that not all crypto backends are `no_std`-compatible.
84//!
85//! [JWT]: https://jwt.io/
86//! [switching]: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
87//! [JWT header]: https://tools.ietf.org/html/rfc7519#section-5
88//! [`alg` field]: https://tools.ietf.org/html/rfc7515#section-4.1.1
89//! [`cty` field]: https://tools.ietf.org/html/rfc7515#section-4.1.10
90//! [CBOR]: https://tools.ietf.org/html/rfc7049
91//! [`sha2`]: https://docs.rs/sha2/
92//! [`libsodium`]: https://download.libsodium.org/doc/
93//! [`exonum-crypto`]: https://docs.rs/exonum-crypto/
94//! [`ed25519-dalek`]: https://doc.dalek.rs/ed25519_dalek/
95//! [`ed25519-compact`]: https://crates.io/crates/ed25519-compact
96//! [`secp256k1`]: https://docs.rs/secp256k1/
97//! [`libsecp256k1`]: https://github.com/bitcoin-core/secp256k1
98//! [`k256`]: https://docs.rs/k256/
99//! [`p256`]: https://docs.rs/p256/
100//! [`rsa`]: https://docs.rs/rsa/
101//! [`chrono`]: https://docs.rs/chrono/
102//! [`getrandom`]: https://docs.rs/getrandom/
103//! [source code repository]: https://github.com/slowli/jwt-compact
104//!
105//! # Examples
106//!
107//! Basic JWT lifecycle:
108//!
109//! ```
110//! use chrono::{Duration, Utc};
111//! use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
112//! use serde::{Serialize, Deserialize};
113//!
114//! /// Custom claims encoded in the token.
115//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
116//! struct CustomClaims {
117//! /// `sub` is a standard claim which denotes claim subject:
118//! /// https://tools.ietf.org/html/rfc7519#section-4.1.2
119//! #[serde(rename = "sub")]
120//! subject: String,
121//! }
122//!
123//! # fn main() -> anyhow::Result<()> {
124//! // Choose time-related options for token creation / validation.
125//! let time_options = TimeOptions::default();
126//! // Create a symmetric HMAC key, which will be used both to create and verify tokens.
127//! let key = Hs256Key::new(b"super_secret_key_donut_steel");
128//! // Create a token.
129//! let header = Header::empty().with_key_id("my-key");
130//! let claims = Claims::new(CustomClaims { subject: "alice".to_owned() })
131//! .set_duration_and_issuance(&time_options, Duration::days(7))
132//! .set_not_before(Utc::now() - Duration::hours(1));
133//! let token_string = Hs256.token(&header, &claims, &key)?;
134//! println!("token: {token_string}");
135//!
136//! // Parse the token.
137//! let token = UntrustedToken::new(&token_string)?;
138//! // Before verifying the token, we might find the key which has signed the token
139//! // using the `Header.key_id` field.
140//! assert_eq!(token.header().key_id, Some("my-key".to_owned()));
141//! // Validate the token integrity.
142//! let token: Token<CustomClaims> = Hs256.validator(&key).validate(&token)?;
143//! // Validate additional conditions.
144//! token.claims()
145//! .validate_expiration(&time_options)?
146//! .validate_maturity(&time_options)?;
147//! // Now, we can extract information from the token (e.g., its subject).
148//! let subject = &token.claims().custom.subject;
149//! assert_eq!(subject, "alice");
150//! # Ok(())
151//! # } // end main()
152//! ```
153//!
154//! ## Compact JWT
155//!
156//! ```
157//! # use chrono::Duration;
158//! # use hex_buffer_serde::{Hex as _, HexForm};
159//! # use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
160//! # use serde::{Serialize, Deserialize};
161//! /// Custom claims encoded in the token.
162//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
163//! struct CustomClaims {
164//! /// `sub` is a standard claim which denotes claim subject:
165//! /// https://tools.ietf.org/html/rfc7519#section-4.1.2
166//! /// The custom serializer we use allows to efficiently
167//! /// encode the subject in CBOR.
168//! #[serde(rename = "sub", with = "HexForm")]
169//! subject: [u8; 32],
170//! }
171//!
172//! # fn main() -> anyhow::Result<()> {
173//! let time_options = TimeOptions::default();
174//! let key = Hs256Key::new(b"super_secret_key_donut_steel");
175//! let claims = Claims::new(CustomClaims { subject: [111; 32] })
176//! .set_duration_and_issuance(&time_options, Duration::days(7));
177//! let token = Hs256.token(&Header::empty(), &claims, &key)?;
178//! println!("token: {token}");
179//! let compact_token = Hs256.compact_token(&Header::empty(), &claims, &key)?;
180//! println!("compact token: {compact_token}");
181//! // The compact token should be ~40 chars shorter.
182//!
183//! // Parse the compact token.
184//! let token = UntrustedToken::new(&compact_token)?;
185//! let token: Token<CustomClaims> = Hs256.validator(&key).validate(&token)?;
186//! token.claims().validate_expiration(&time_options)?;
187//! // Now, we can extract information from the token (e.g., its subject).
188//! assert_eq!(token.claims().custom.subject, [111; 32]);
189//! # Ok(())
190//! # } // end main()
191//! ```
192//!
193//! ## JWT with custom header fields
194//!
195//! ```
196//! # use chrono::Duration;
197//! # use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
198//! # use serde::{Deserialize, Serialize};
199//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
200//! struct CustomClaims { subject: [u8; 32] }
201//!
202//! /// Additional fields in the token header.
203//! #[derive(Debug, Clone, Serialize, Deserialize)]
204//! struct HeaderExtensions { custom: bool }
205//!
206//! # fn main() -> anyhow::Result<()> {
207//! let time_options = TimeOptions::default();
208//! let key = Hs256Key::new(b"super_secret_key_donut_steel");
209//! let claims = Claims::new(CustomClaims { subject: [111; 32] })
210//! .set_duration_and_issuance(&time_options, Duration::days(7));
211//! let header = Header::new(HeaderExtensions { custom: true })
212//! .with_key_id("my-key");
213//! let token = Hs256.token(&header, &claims, &key)?;
214//! print!("token: {token}");
215//!
216//! // Parse the token.
217//! let token: UntrustedToken<HeaderExtensions> =
218//! token.as_str().try_into()?;
219//! // Token header (incl. custom fields) can be accessed right away.
220//! assert_eq!(token.header().key_id.as_deref(), Some("my-key"));
221//! assert!(token.header().other_fields.custom);
222//! // Token can then be validated as usual.
223//! let token = Hs256.validator::<CustomClaims>(&key).validate(&token)?;
224//! assert_eq!(token.claims().custom.subject, [111; 32]);
225//! # Ok(())
226//! # } // end main()
227//! ```
228
229#![cfg_attr(not(feature = "std"), no_std)]
230// Documentation settings.
231#![cfg_attr(docsrs, feature(doc_cfg))]
232#![doc(html_root_url = "https://docs.rs/jwt-compact/0.9.0-beta.1")]
233// Linter settings.
234#![warn(missing_debug_implementations, missing_docs, bare_trait_objects)]
235#![warn(clippy::all, clippy::pedantic)]
236#![allow(
237 clippy::missing_errors_doc,
238 clippy::must_use_candidate,
239 clippy::module_name_repetitions
240)]
241
242pub use crate::{
243 claims::{Claims, Empty, TimeOptions},
244 error::{Claim, CreationError, ParseError, ValidationError},
245 token::{Header, SignedToken, Thumbprint, Token, UntrustedToken},
246 traits::{Algorithm, AlgorithmExt, AlgorithmSignature, Renamed, Validator},
247};
248
249pub mod alg;
250mod claims;
251mod error;
252pub mod jwk;
253mod token;
254mod traits;
255
256// Polyfill for `alloc` types.
257mod alloc {
258 #[cfg(not(feature = "std"))]
259 extern crate alloc as std;
260
261 #[cfg(feature = "rsa")]
262 pub use std::boxed::Box;
263 pub use std::{
264 borrow::{Cow, ToOwned},
265 format,
266 string::{String, ToString},
267 vec::Vec,
268 };
269}
270
271/// Prelude to neatly import all necessary stuff from the crate.
272pub mod prelude {
273 #[doc(no_inline)]
274 pub use crate::{AlgorithmExt as _, Claims, Header, TimeOptions, Token, UntrustedToken};
275}
276
277#[cfg(doctest)]
278doc_comment::doctest!("../README.md");