Skip to main content

jwt/
jwt.rs

1//! Sign, parse, and verify JSON Web Tokens (JWT).
2//!
3//! # Two-step verification flow
4//!
5//! [`parse_header`] and [`parse_and_verify`] are separate so that you can
6//! inspect the header, especially the `kid` field (Key ID), before choosing
7//! which key to verify with. This is essential when the key must be looked up dynamically.
8//!
9//! # `no_std` support
10//!
11//! Disable default features (`default-features = false`) and provide your own
12//! [`Clock`] implementation for time-based claim verification (optional).
13//!
14//! # Example
15//!
16//! ```
17//! use jwt::*;
18//!
19//! fn main() -> Result<(), Error> {
20//!     // 1. Generate an Ed25519 secret key.
21//!     let secret_key = Ed25519SecretKey::generate();
22//!
23//!     // 2. Build the header.
24//!     let header = Header {
25//!         typ: TokenType::JWT,
26//!         alg: Algorithm::EdDSA,
27//!         kid: Some("my-key-id".into()),
28//!         cty: None,
29//!         jku: None,
30//!         x5u: None,
31//!         x5c: None,
32//!         x5t: None,
33//!         x5t_s256: None,
34//!     };
35//!
36//!     // 3. Define the claims and sign.
37//!     let claims = serde_json::json!({ "sub": "user123", "exp": 9999999999_u64 });
38//!     let token = sign(&secret_key, &header, &claims)?;
39//!
40//!     // 4. Extract the public key for verification.
41//!     let public_key = secret_key.public_key();
42//!
43//!     // 5. Parse only the header first (e.g., to read `kid` for key lookup).
44//!     let parsed_header = parse_header(&token)?;
45//!     assert_eq!(parsed_header.kid.as_deref(), Some("my-key-id"));
46//!
47//!     // 6. Now verify and deserialize the claims.
48//!     let opts = VerifyOptions {
49//!         allowed_time_drift: core::time::Duration::from_secs(60),
50//!         exp: true,
51//!         nbf: false,
52//!         aud: None,
53//!         iss: None,
54//!         clock: Some(&jwt::SYSTEM_CLOCK),
55//!     };
56//!     let claims: serde_json::Value =
57//!         parse_and_verify(&public_key, &parsed_header, &token, &opts)?;
58//!     assert_eq!(claims["sub"], "user123");
59//!
60//!     Ok(())
61//! }
62//! ```
63//!
64//! # JSON Web Key (JWK) import and export
65//!
66//! Keys can be exported to and imported from JSON Web Keys (JWK) for publishing or later re-use
67//! with [`Jwk::from`].
68//!
69//! The [`Key`] enum represents any supported cryptographic key. Convert a JWK
70//! into a [`Key`] with [`Key::try_from`]`(&Jwk)`, then use it directly with
71//! [`sign`] or [`parse_and_verify`] without knowing the concrete key type.
72//!
73//! ```
74//! use jwt::*;
75//!
76//! fn main() -> Result<(), Error> {
77//!     // Generate a P-256 key pair.
78//!     let secret_key = P256SecretKey::generate()?;
79//!     let public_key = secret_key.public_key();
80//!
81//!     // Convert to a JWK (e.g. for publishing in a JWKS endpoint).
82//!     let jwk = Jwk::from(&public_key);
83//!     assert_eq!(jwk.algorithm, Algorithm::ES256);
84//!
85//!     // Parse the JWK back into a Key (e.g. from a JWKS response).
86//!     let key = Key::try_from(&jwk)?;
87//!
88//!     // sign and parse_and_verify accept &Key directly.
89//!     let header = Header { typ: TokenType::JWT, alg: Algorithm::ES256, ..Default::default() };
90//!     let token = sign(&secret_key, &header, &serde_json::json!({"exp": 9999999999_u64, "nbf": 0_u64 }))?;
91//!     let parsed_header = parse_header(&token)?;
92//!     let _claims: serde_json::Value = parse_and_verify(&key, &parsed_header, &token, &VerifyOptions::default())?;
93//!
94//!     Ok(())
95//! }
96//! ```
97
98#![cfg_attr(not(feature = "std"), no_std)]
99
100extern crate alloc;
101
102use alloc::{
103    string::{String, ToString},
104    vec::Vec,
105};
106use core::time::Duration;
107
108use serde::{Deserialize, Serialize, de::DeserializeOwned};
109use small_collections::SmallString;
110
111mod jwk;
112mod jwt_crypto;
113
114pub use jwk::*;
115pub use jwt_crypto::*;
116
117#[cfg(feature = "std")]
118pub static SYSTEM_CLOCK: SystemClock = SystemClock;
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct Header {
122    /// The only valid value is "JWT"
123    /// https://tools.ietf.org/html/rfc7519#section-5.1
124    pub typ: TokenType,
125
126    /// ttps://tools.ietf.org/html/rfc7515#section-4.1.1
127    pub alg: Algorithm,
128
129    /// Content type
130    /// https://tools.ietf.org/html/rfc7519#section-5.2
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub cty: Option<SmallString<3>>,
133
134    /// JSON Key URL
135    /// https://tools.ietf.org/html/rfc7515#section-4.1.2
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub jku: Option<String>,
138
139    /// JSON Web Key
140    /// https://tools.ietf.org/html/rfc7515#section-4.1.3
141    // #[serde(skip_serializing_if = "Option::is_none")]
142    // pub jwk: Option<Jwk>,
143
144    /// Key ID
145    /// https://tools.ietf.org/html/rfc7515#section-4.1.4
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub kid: Option<SmallString<36>>, // 36 = UUID length
148
149    /// X.509 URL
150    /// https://tools.ietf.org/html/rfc7515#section-4.1.5
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub x5u: Option<String>,
153
154    /// X.509 certificate chain.
155    /// https://tools.ietf.org/html/rfc7515#section-4.1.6
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub x5c: Option<Vec<String>>,
158
159    /// X.509 SHA1 certificate Thumbprint
160    /// https://tools.ietf.org/html/rfc7515#section-4.1.7
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub x5t: Option<SmallString<27>>, // 27 = base64_encoded_length(20)
163
164    /// X.509 SHA256 certificate Thumbprint
165    /// https://tools.ietf.org/html/rfc7515#section-4.1.8
166    #[serde(skip_serializing_if = "Option::is_none")]
167    #[serde(rename = "x5t#S256")]
168    pub x5t_s256: Option<SmallString<43>>, // 43 = base64_encoded_length(32)
169}
170
171impl Default for Header {
172    fn default() -> Self {
173        Self {
174            typ: TokenType::JWT,
175            alg: Algorithm::EdDSA,
176            cty: None,
177            jku: None,
178            kid: None,
179            x5u: None,
180            x5c: None,
181            x5t: None,
182            x5t_s256: None,
183        }
184    }
185}
186
187/// Registered claim names from https://www.rfc-editor.org/rfc/rfc7519#section-4.1
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
189pub struct RegisteredClaims {
190    /// Issuer
191    /// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub iss: Option<SmallString<20>>,
194
195    /// Subject
196    /// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub sub: Option<SmallString<36>>, // 36 = UUID length
199
200    /// Audience
201    /// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub aud: Option<SmallString<20>>,
204
205    /// Expiration Time
206    /// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub exp: Option<u64>,
209
210    /// Not Before
211    /// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub nbf: Option<u64>,
214
215    /// Issued At
216    /// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub iat: Option<u64>,
219
220    /// JWT ID
221    /// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub jti: Option<SmallString<36>>,
224}
225
226#[derive(Debug, Default, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
227pub enum TokenType {
228    #[default]
229    JWT,
230}
231
232/// The algorithms supported for signing / verifying JWTs
233#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
234pub enum Algorithm {
235    /// HMAC using SHA-256
236    HS256,
237
238    /// HMAC using SHA-384
239    HS384,
240
241    /// HMAC using SHA-512
242    HS512,
243
244    /// BLAKE3 in keyed mode
245    BLAKE3,
246
247    /// Edwards-curve Digital Signature Algorithm (EdDSA)
248    EdDSA,
249
250    /// ECDSA using P-256 and SHA-256
251    ES256,
252
253    /// ECDSA using P-384 and SHA-384
254    ES384,
255
256    /// ECDSA using P-521 and SHA-512
257    ES512,
258
259    /// ML-DSA-44
260    MlDsa44,
261
262    /// ML-DSA-65
263    MlDsa65,
264
265    /// RSASSA-PKCS1-v1.5 with SHA-256
266    RS256,
267
268    /// RSASSA-PKCS1-v1.5 with SHA-384
269    RS384,
270
271    /// RSASSA-PKCS1-v1.5 with SHA-512
272    RS512,
273
274    /// RSASSA-PSS with SHA-256
275    PS256,
276
277    /// RSASSA-PSS with SHA-384
278    PS384,
279
280    /// RSASSA-PSS with SHA-512
281    PS512,
282}
283
284impl Algorithm {
285    /// Returns the size of the signature of the algorithm, or, if the size can vary in size,
286    /// the upper bound supported by this package (e.g. 4096 bits / 512 bytes for RSA).
287    /// This is used, among other things, to pre-allocate the output buffer to the correct size
288    /// when encoding a JWT.
289    #[inline]
290    pub(crate) fn signature_max_size(&self) -> usize {
291        match self {
292            Algorithm::BLAKE3 => 32,
293            Algorithm::HS256 => 32,
294            Algorithm::HS384 => 48,
295            Algorithm::HS512 => 64,
296            Algorithm::EdDSA => 64,
297            Algorithm::ES256 => 64,
298            Algorithm::ES384 => 96,
299            Algorithm::ES512 => 132,
300            Algorithm::RS256 => 512,
301            Algorithm::RS384 => 512,
302            Algorithm::RS512 => 512,
303            Algorithm::PS256 => 512,
304            Algorithm::PS384 => 512,
305            Algorithm::PS512 => 512,
306            Algorithm::MlDsa44 => 2420,
307            Algorithm::MlDsa65 => 3309,
308            // Algorithm::MlDsa87 => 4627,
309        }
310    }
311}
312
313impl core::str::FromStr for Algorithm {
314    type Err = Error;
315
316    fn from_str(s: &str) -> Result<Self, Self::Err> {
317        match s {
318            "BLAKE3" => Ok(Algorithm::BLAKE3),
319            "HS256" => Ok(Algorithm::HS256),
320            "HS384" => Ok(Algorithm::HS384),
321            "HS512" => Ok(Algorithm::HS512),
322            "ES256" => Ok(Algorithm::ES256),
323            "ES384" => Ok(Algorithm::ES384),
324            "ES512" => Ok(Algorithm::ES512),
325            "EdDSA" => Ok(Algorithm::EdDSA),
326            "ML-DSA-44" => Ok(Algorithm::MlDsa44),
327            "ML-DSA-65" => Ok(Algorithm::MlDsa65),
328            "RS256" => Ok(Algorithm::RS256),
329            "RS384" => Ok(Algorithm::RS384),
330            "RS512" => Ok(Algorithm::RS512),
331            "PS256" => Ok(Algorithm::PS256),
332            "PS384" => Ok(Algorithm::PS384),
333            "PS512" => Ok(Algorithm::PS512),
334            _ => Err(Error::UnknownAlgorithm(s.to_string())),
335        }
336    }
337}
338
339impl core::fmt::Display for Algorithm {
340    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341        write!(f, "{self:?}")
342    }
343}
344
345#[derive(Debug)]
346pub enum Error {
347    UnknownAlgorithm(String),
348    InvalidCurve,
349    InvalidTokenType(String),
350    Json(serde_json::Error),
351    InvalidToken,
352    InvalidSignature,
353    InvalidKey,
354    InvalidEllipticCurve(String),
355    InvalidJwk { kid: String, err: String },
356    Unspecified(String),
357    ClockIsMissing,
358}
359
360impl core::fmt::Display for Error {
361    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
362        match self {
363            Error::UnknownAlgorithm(algorithm) => write!(f, "unknown algorithm: {algorithm}"),
364            Error::InvalidCurve => f.write_str("invalid curve"),
365            Error::InvalidTokenType(token_type) => write!(f, "invalid token type: {token_type}"),
366            Error::Json(err) => write!(f, "error serializing JWT to JSON: {err}"),
367            Error::InvalidToken => f.write_str("JWT is not valid"),
368            Error::InvalidSignature => f.write_str("signature is not valid"),
369            Error::InvalidKey => f.write_str("key is not valid"),
370            Error::InvalidEllipticCurve(curve) => write!(f, "invalid elliptic curve: {curve}"),
371            Error::InvalidJwk {
372                kid,
373                err,
374            } => write!(f, "{kid} is not a valid JWK: {err}"),
375            Error::Unspecified(err) => f.write_str(&err),
376            Error::ClockIsMissing => f.write_str("a clock is needed for exp or nbf verification"),
377        }
378    }
379}
380
381impl From<serde_json::Error> for Error {
382    fn from(err: serde_json::Error) -> Self {
383        Self::Json(err)
384    }
385}
386
387#[cfg(feature = "std")]
388impl std::error::Error for Error {}
389
390/// A wall clock used to check tokens expiration.
391///
392/// Returns the current Unix timestamp (seconds since epoch). This trait
393/// exists so that no_std environments can inject their own time source
394/// (hardware RTC, NTP, etc.) instead of relying on `std::time::SystemTime`.
395pub trait Clock: Send + Sync {
396    fn now(&self) -> u64;
397}
398
399/// A [`Clock`] backed by `std::time::SystemTime`.
400///
401/// Available only when the `std` feature is enabled.
402#[cfg(feature = "std")]
403pub struct SystemClock;
404
405#[cfg(feature = "std")]
406impl Clock for SystemClock {
407    #[inline]
408    fn now(&self) -> u64 {
409        std::time::SystemTime::now()
410            .duration_since(std::time::UNIX_EPOCH)
411            .unwrap_or_default()
412            .as_secs()
413    }
414}
415
416pub struct VerifyOptions<'a> {
417    /// Allowed time drift for `nbf` and `exp` verification in order to account for devices with
418    /// inacurate clocks.
419    /// default: 30 seconds
420    pub allowed_time_drift: Duration,
421    /// default: true
422    pub nbf: bool,
423    /// default: true
424    pub exp: bool,
425    /// default: None
426    pub aud: Option<&'a [&'a str]>,
427    /// default: None
428    pub iss: Option<&'a [&'a str]>,
429    /// default: Some(&SYSTEM_CLOCK)
430    pub clock: Option<&'a dyn Clock>,
431}
432
433#[cfg(feature = "std")]
434impl Default for VerifyOptions<'_> {
435    fn default() -> Self {
436        Self {
437            allowed_time_drift: core::time::Duration::from_secs(30),
438            nbf: true,
439            exp: true,
440            aud: None,
441            iss: None,
442            clock: Some(&SYSTEM_CLOCK),
443        }
444    }
445}
446
447pub fn sign<C: Serialize>(key: &dyn Signer, header: &Header, claims: &C) -> Result<String, Error> {
448    let signing_algorithm = key.algorithm();
449    if signing_algorithm != header.alg {
450        return Err(Error::InvalidKey);
451    }
452
453    let header_base64 = base64::encode(serde_json::to_string(header)?.as_bytes(), base64::Alphabet::UrlNoPadding);
454    let claims_base64 = base64::encode(serde_json::to_string(claims)?.as_bytes(), base64::Alphabet::UrlNoPadding);
455
456    let mut jwt = String::with_capacity(
457        header_base64.len()
458            + claims_base64.len()
459            + base64::encoded_length(signing_algorithm.signature_max_size(), false)
460                .expect("error getting base64 encoding length")
461            + 2,
462    );
463    jwt.push_str(&header_base64);
464    jwt.push('.');
465    jwt.push_str(&claims_base64);
466
467    let signature = key.sign(jwt.as_bytes())?;
468    jwt.push('.');
469    jwt.push_str(&base64::encode(signature.as_ref(), base64::Alphabet::UrlNoPadding));
470
471    return Ok(jwt);
472}
473
474pub fn parse_header(token: &str) -> Result<Header, Error> {
475    let mut parts = token.split('.');
476    let header_base64 = parts.next().ok_or(Error::InvalidToken)?;
477    if parts.count() != 2 {
478        return Err(Error::InvalidToken);
479    }
480
481    let header_json = base64::decode(header_base64, base64::Alphabet::UrlNoPadding).map_err(|_| Error::InvalidToken)?;
482    let header: Header = serde_json::from_slice(&header_json).map_err(|_| Error::InvalidToken)?;
483
484    return Ok(header);
485}
486
487pub fn parse_and_verify<C: DeserializeOwned>(
488    key: &dyn Verifier,
489    header: &Header,
490    token: &str,
491    verify_options: &VerifyOptions,
492) -> Result<C, Error> {
493    if (verify_options.exp || verify_options.nbf) && verify_options.clock.is_none() {
494        return Err(Error::ClockIsMissing);
495    }
496
497    if header.alg != key.algorithm() {
498        return Err(Error::InvalidToken);
499    }
500
501    let mut parts = token.split('.');
502    let header_base64 = parts.next().ok_or(Error::InvalidToken)?;
503    let claims_base64 = parts.next().ok_or(Error::InvalidToken)?;
504    let signature_base64 = parts.next().ok_or(Error::InvalidToken)?;
505    if parts.next().is_some() {
506        return Err(Error::InvalidToken);
507    }
508
509    let mut signature_buffer = [0u8; SIGNATURE_MAX_SIZE];
510    let signature_size = base64::decode_into(
511        &mut signature_buffer,
512        signature_base64.as_bytes(),
513        base64::Alphabet::UrlNoPadding,
514    )
515    .map_err(|_| Error::InvalidSignature)?;
516
517    let signed_message = &token[..header_base64.len() + 1 + claims_base64.len()].as_bytes();
518    key.verify(signed_message, &signature_buffer[..signature_size])
519        .map_err(|_| Error::InvalidSignature)?;
520
521    let claims_json =
522        base64::decode(&claims_base64, base64::Alphabet::UrlNoPadding).map_err(|_| Error::InvalidToken)?;
523
524    let claims =
525        if verify_options.exp || verify_options.nbf || verify_options.aud.is_some() || verify_options.iss.is_some() {
526            let claims_json_value: serde_json::Value =
527                serde_json::from_slice(&claims_json).map_err(|_| Error::InvalidToken)?;
528
529            match &claims_json_value {
530                serde_json::Value::Object(claims_object) => {
531                    if verify_options.exp {
532                        match claims_object.get("exp") {
533                            None => return Err(Error::InvalidToken),
534                            Some(exp_value) => {
535                                if let Some(exp) = exp_value.as_u64() {
536                                    let now = verify_options.clock.unwrap().now();
537                                    if exp < (now - verify_options.allowed_time_drift.as_secs()) {
538                                        return Err(Error::InvalidToken);
539                                    }
540                                } else {
541                                    return Err(Error::InvalidToken);
542                                }
543                            }
544                        }
545                    }
546
547                    if verify_options.nbf {
548                        match claims_object.get("nbf") {
549                            None => return Err(Error::InvalidToken),
550                            Some(nbf_value) => {
551                                if let Some(nbf) = nbf_value.as_u64() {
552                                    let now = verify_options.clock.unwrap().now();
553                                    if nbf > (now + verify_options.allowed_time_drift.as_secs()) {
554                                        return Err(Error::InvalidToken);
555                                    }
556                                } else {
557                                    return Err(Error::InvalidToken);
558                                }
559                            }
560                        }
561                    }
562
563                    if let Some(expected_aud) = verify_options.aud {
564                        match claims_object.get("aud") {
565                            None => return Err(Error::InvalidToken),
566                            Some(aud_value) => {
567                                if let Some(aud) = aud_value.as_str() {
568                                    if !expected_aud.contains(&aud) {
569                                        return Err(Error::InvalidToken);
570                                    }
571                                } else {
572                                    return Err(Error::InvalidToken);
573                                }
574                            }
575                        }
576                    }
577
578                    if let Some(expected_iss) = verify_options.iss {
579                        match claims_object.get("iss") {
580                            None => return Err(Error::InvalidToken),
581                            Some(iss_value) => {
582                                if let Some(iss) = iss_value.as_str() {
583                                    if !expected_iss.contains(&iss) {
584                                        return Err(Error::InvalidToken);
585                                    }
586                                } else {
587                                    return Err(Error::InvalidToken);
588                                }
589                            }
590                        }
591                    }
592                }
593                _ => return Err(Error::InvalidToken),
594            };
595
596            serde_json::from_value(claims_json_value).map_err(|_| Error::InvalidToken)?
597        } else {
598            serde_json::from_slice(&claims_json).map_err(|_| Error::InvalidToken)?
599        };
600
601    return Ok(claims);
602}