jwt_compact/
token.rs

1//! `Token` and closely related types.
2
3use alloc::{borrow::Cow, format, string::String, vec::Vec};
4use core::{cmp, fmt};
5
6use base64ct::{Base64UrlUnpadded, Encoding};
7use serde::{
8    Deserialize, Deserializer, Serialize, Serializer,
9    de::{DeserializeOwned, Error as DeError, Visitor},
10};
11use smallvec::{SmallVec, smallvec};
12
13#[cfg(feature = "ciborium")]
14use crate::error::CborDeError;
15use crate::{Algorithm, Claims, Empty, ParseError, ValidationError};
16
17/// Maximum "reasonable" signature size in bytes.
18const SIGNATURE_SIZE: usize = 128;
19
20/// Representation of a X.509 certificate thumbprint (`x5t` and `x5t#S256` fields in
21/// the JWT [`Header`]).
22///
23/// As per the JWS spec in [RFC 7515], a certificate thumbprint (i.e., the SHA-1 / SHA-256
24/// digest of the certificate) must be base64url-encoded. Some JWS implementations however
25/// encode not the thumbprint itself, but rather its hex encoding, sometimes even
26/// with additional chars spliced within. To account for these implementations,
27/// a thumbprint is represented as an enum – either a properly encoded hash digest,
28/// or an opaque base64-encoded string.
29///
30/// [RFC 7515]: https://www.rfc-editor.org/rfc/rfc7515.html
31///
32/// # Examples
33///
34/// ```
35/// # use assert_matches::assert_matches;
36/// # use jwt_compact::{
37/// #     alg::{Hs256, Hs256Key}, AlgorithmExt, Claims, Header, Thumbprint, UntrustedToken,
38/// # };
39/// # fn main() -> anyhow::Result<()> {
40/// let key = Hs256Key::new(b"super_secret_key_donut_steel");
41///
42/// // Creates a token with a custom-encoded SHA-1 thumbprint.
43/// let thumbprint = "65:AF:69:09:B1:B0:75:8E:06:C6:E0:48:C4:60:02:B5:C6:95:E3:6B";
44/// let header = Header::empty()
45///     .with_key_id("my_key")
46///     .with_certificate_sha1_thumbprint(thumbprint);
47/// let token = Hs256.token(&header, &Claims::empty(), &key)?;
48/// println!("{token}");
49///
50/// // Deserialize the token and check that its header fields are readable.
51/// let token = UntrustedToken::new(&token)?;
52/// let deserialized_thumbprint =
53///     token.header().certificate_sha1_thumbprint.as_ref();
54/// assert_matches!(
55///     deserialized_thumbprint,
56///     Some(Thumbprint::String(s)) if s == thumbprint
57/// );
58/// # Ok(())
59/// # }
60/// ```
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62#[non_exhaustive]
63pub enum Thumbprint<const N: usize> {
64    /// Byte representation of a SHA-1 or SHA-256 digest.
65    Bytes([u8; N]),
66    /// Opaque string representation of the thumbprint. It is the responsibility
67    /// of an application to verify that this value is valid.
68    String(String),
69}
70
71impl<const N: usize> From<[u8; N]> for Thumbprint<N> {
72    fn from(value: [u8; N]) -> Self {
73        Self::Bytes(value)
74    }
75}
76
77impl<const N: usize> From<String> for Thumbprint<N> {
78    fn from(s: String) -> Self {
79        Self::String(s)
80    }
81}
82
83impl<const N: usize> From<&str> for Thumbprint<N> {
84    fn from(s: &str) -> Self {
85        Self::String(s.into())
86    }
87}
88
89impl<const N: usize> Serialize for Thumbprint<N> {
90    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
91        let input = match self {
92            Self::Bytes(bytes) => bytes.as_slice(),
93            Self::String(s) => s.as_bytes(),
94        };
95        serializer.serialize_str(&Base64UrlUnpadded::encode_string(input))
96    }
97}
98
99impl<'de, const N: usize> Deserialize<'de> for Thumbprint<N> {
100    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101        struct Base64Visitor<const L: usize>;
102
103        impl<const L: usize> Visitor<'_> for Base64Visitor<L> {
104            type Value = Thumbprint<L>;
105
106            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107                write!(formatter, "base64url-encoded thumbprint")
108            }
109
110            fn visit_str<E: DeError>(self, mut value: &str) -> Result<Self::Value, E> {
111                // Allow for padding. RFC 7515 defines base64url encoding as one without padding:
112                //
113                // > Base64url Encoding: Base64 encoding using the URL- and filename-safe
114                // > character set defined in Section 5 of RFC 4648 [RFC4648], with all trailing '='
115                // > characters omitted [...]
116                //
117                // ...but it's easy to trim the padding, so we support it anyway.
118                //
119                // See: https://www.rfc-editor.org/rfc/rfc7515.html#section-2
120                for _ in 0..2 {
121                    if value.as_bytes().last() == Some(&b'=') {
122                        value = &value[..value.len() - 1];
123                    }
124                }
125
126                let decoded_len = value.len() * 3 / 4;
127                match decoded_len.cmp(&L) {
128                    cmp::Ordering::Less => Err(E::custom(format!(
129                        "thumbprint must contain at least {L} bytes"
130                    ))),
131                    cmp::Ordering::Equal => {
132                        let mut bytes = [0_u8; L];
133                        let len = Base64UrlUnpadded::decode(value, &mut bytes)
134                            .map_err(E::custom)?
135                            .len();
136                        debug_assert_eq!(len, L);
137                        Ok(bytes.into())
138                    }
139                    cmp::Ordering::Greater => {
140                        let decoded = Base64UrlUnpadded::decode_vec(value).map_err(E::custom)?;
141                        let decoded = String::from_utf8(decoded)
142                            .map_err(|err| E::custom(err.utf8_error()))?;
143                        Ok(decoded.into())
144                    }
145                }
146            }
147        }
148
149        deserializer.deserialize_str(Base64Visitor)
150    }
151}
152
153/// JWT header.
154///
155/// See [RFC 7515](https://tools.ietf.org/html/rfc7515#section-4.1) for the description
156/// of the fields. The purpose of all fields except `token_type` is to determine
157/// the verifying key. Since these values will be provided by the adversary in the case of
158/// an attack, they require additional verification (e.g., a provided certificate might
159/// be checked against the list of "acceptable" certificate authorities).
160///
161/// A `Header` can be created using `Default` implementation, which does not set any fields.
162/// For added fluency, you may use `with_*` methods:
163///
164/// ```
165/// # use jwt_compact::Header;
166/// use sha2::{digest::Digest, Sha256};
167///
168/// let my_key_cert = // DER-encoded key certificate
169/// #   b"Hello, world!";
170/// let thumbprint: [u8; 32] = Sha256::digest(my_key_cert).into();
171/// let header = Header::empty()
172///     .with_key_id("my-key-id")
173///     .with_certificate_thumbprint(thumbprint);
174/// ```
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176#[non_exhaustive]
177pub struct Header<T = Empty> {
178    /// URL of the JSON Web Key Set containing the key that has signed the token.
179    /// This field is renamed to [`jku`] for serialization.
180    ///
181    /// [`jku`]: https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.2
182    #[serde(rename = "jku", default, skip_serializing_if = "Option::is_none")]
183    pub key_set_url: Option<String>,
184
185    /// Identifier of the key that has signed the token. This field is renamed to [`kid`]
186    /// for serialization.
187    ///
188    /// [`kid`]: https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.4
189    #[serde(rename = "kid", default, skip_serializing_if = "Option::is_none")]
190    pub key_id: Option<String>,
191
192    /// URL of the X.509 certificate for the signing key. This field is renamed to [`x5u`]
193    /// for serialization.
194    ///
195    /// [`x5u`]: https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.5
196    #[serde(rename = "x5u", default, skip_serializing_if = "Option::is_none")]
197    pub certificate_url: Option<String>,
198
199    /// SHA-1 thumbprint of the X.509 certificate for the signing key.
200    /// This field is renamed to [`x5t`] for serialization.
201    ///
202    /// [`x5t`]: https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.7
203    #[serde(rename = "x5t", default, skip_serializing_if = "Option::is_none")]
204    pub certificate_sha1_thumbprint: Option<Thumbprint<20>>,
205
206    /// SHA-256 thumbprint of the X.509 certificate for the signing key.
207    /// This field is renamed to [`x5t#S256`] for serialization.
208    ///
209    /// [`x5t#S256`]: https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.8
210    #[serde(rename = "x5t#S256", default, skip_serializing_if = "Option::is_none")]
211    pub certificate_thumbprint: Option<Thumbprint<32>>,
212
213    /// Application-specific [token type]. This field is renamed to `typ` for serialization.
214    ///
215    /// [token type]: https://tools.ietf.org/html/rfc7519#section-5.1
216    #[serde(rename = "typ", default, skip_serializing_if = "Option::is_none")]
217    pub token_type: Option<String>,
218
219    /// Other fields encoded in the header. These fields may be used by agreement between
220    /// the producer and consumer of the token to pass additional information.
221    /// See Sections 4.2 and 4.3 of [RFC 7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.2)
222    /// for details.
223    ///
224    /// For the token creation and validation to work properly, the fields type must [`Serialize`]
225    /// to a JSON object.
226    ///
227    /// Note that these fields do not include the signing algorithm (`alg`) and the token
228    /// content type (`cty`) since both these fields have predefined semantics and are used
229    /// internally by the crate logic.
230    #[serde(flatten)]
231    pub other_fields: T,
232}
233
234impl Header {
235    /// Creates an empty header.
236    pub const fn empty() -> Self {
237        Self {
238            key_set_url: None,
239            key_id: None,
240            certificate_url: None,
241            certificate_sha1_thumbprint: None,
242            certificate_thumbprint: None,
243            token_type: None,
244            other_fields: Empty {},
245        }
246    }
247}
248
249impl<T> Header<T> {
250    /// Creates a header with the specified custom fields.
251    pub const fn new(fields: T) -> Header<T> {
252        Header {
253            key_set_url: None,
254            key_id: None,
255            certificate_url: None,
256            certificate_sha1_thumbprint: None,
257            certificate_thumbprint: None,
258            token_type: None,
259            other_fields: fields,
260        }
261    }
262
263    /// Sets the `key_set_url` field for this header.
264    #[must_use]
265    pub fn with_key_set_url(mut self, key_set_url: impl Into<String>) -> Self {
266        self.key_set_url = Some(key_set_url.into());
267        self
268    }
269
270    /// Sets the `key_id` field for this header.
271    #[must_use]
272    pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
273        self.key_id = Some(key_id.into());
274        self
275    }
276
277    /// Sets the `certificate_url` field for this header.
278    #[must_use]
279    pub fn with_certificate_url(mut self, certificate_url: impl Into<String>) -> Self {
280        self.certificate_url = Some(certificate_url.into());
281        self
282    }
283
284    /// Sets the `certificate_sha1_thumbprint` field for this header.
285    #[must_use]
286    pub fn with_certificate_sha1_thumbprint(
287        mut self,
288        certificate_thumbprint: impl Into<Thumbprint<20>>,
289    ) -> Self {
290        self.certificate_sha1_thumbprint = Some(certificate_thumbprint.into());
291        self
292    }
293
294    /// Sets the `certificate_thumbprint` field for this header.
295    #[must_use]
296    pub fn with_certificate_thumbprint(
297        mut self,
298        certificate_thumbprint: impl Into<Thumbprint<32>>,
299    ) -> Self {
300        self.certificate_thumbprint = Some(certificate_thumbprint.into());
301        self
302    }
303
304    /// Sets the `token_type` field for this header.
305    #[must_use]
306    pub fn with_token_type(mut self, token_type: impl Into<String>) -> Self {
307        self.token_type = Some(token_type.into());
308        self
309    }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub(crate) struct CompleteHeader<'a, T> {
314    #[serde(rename = "alg")]
315    pub algorithm: Cow<'a, str>,
316    #[serde(rename = "cty", default, skip_serializing_if = "Option::is_none")]
317    pub content_type: Option<String>,
318    #[serde(flatten)]
319    pub inner: T,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323enum ContentType {
324    Json,
325    #[cfg(feature = "ciborium")]
326    Cbor,
327}
328
329/// Parsed, but unvalidated token.
330///
331/// The type param ([`Empty`] by default) corresponds to the [additional information] enclosed
332/// in the token [`Header`].
333///
334/// An `UntrustedToken` can be parsed from a string using the [`TryFrom`] implementation.
335/// This checks that a token is well-formed (has a header, claims and a signature),
336/// but does not validate the signature.
337/// As a shortcut, a token without additional header info can be created using [`Self::new()`].
338///
339/// [additional information]: Header#other_fields
340///
341/// # Examples
342///
343/// ```
344/// # use jwt_compact::UntrustedToken;
345/// let token_str = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJp\
346///     c3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leG\
347///     FtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJ\
348///     U1p1r_wW1gFWFOEjXk";
349/// let token: UntrustedToken = token_str.try_into()?;
350/// // The same operation using a shortcut:
351/// let same_token = UntrustedToken::new(token_str)?;
352/// // Token header can be accessed to select the verifying key etc.
353/// let key_id: Option<&str> = token.header().key_id.as_deref();
354/// # Ok::<_, anyhow::Error>(())
355/// ```
356///
357/// ## Handling tokens with custom header fields
358///
359/// ```
360/// # use serde::Deserialize;
361/// # use jwt_compact::UntrustedToken;
362/// #[derive(Debug, Clone, Deserialize)]
363/// struct HeaderExtensions {
364///     custom: String,
365/// }
366///
367/// let token_str = "eyJhbGciOiJIUzI1NiIsImtpZCI6InRlc3Rfa2V5Iiwid\
368///     HlwIjoiSldUIiwiY3VzdG9tIjoiY3VzdG9tIn0.eyJzdWIiOiIxMjM0NTY\
369///     3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9._27Fb6nF\
370///     Tg-HSt3vO4ylaLGcU_ZV2VhMJR4HL7KaQik";
371/// let token: UntrustedToken<HeaderExtensions> = token_str.try_into()?;
372/// let extensions = &token.header().other_fields;
373/// println!("{}", extensions.custom);
374/// # Ok::<_, anyhow::Error>(())
375/// ```
376#[derive(Debug, Clone)]
377pub struct UntrustedToken<'a, H = Empty> {
378    pub(crate) signed_data: Cow<'a, [u8]>,
379    header: Header<H>,
380    algorithm: String,
381    content_type: ContentType,
382    serialized_claims: Vec<u8>,
383    signature: SmallVec<[u8; SIGNATURE_SIZE]>,
384}
385
386/// Token with validated integrity.
387///
388/// Claims encoded in the token can be verified by invoking [`Claims`] methods
389/// via [`Self::claims()`].
390#[derive(Debug, Clone)]
391pub struct Token<T, H = Empty> {
392    header: Header<H>,
393    claims: Claims<T>,
394}
395
396impl<T, H> Token<T, H> {
397    pub(crate) fn new(header: Header<H>, claims: Claims<T>) -> Self {
398        Self { header, claims }
399    }
400
401    /// Gets token header.
402    pub fn header(&self) -> &Header<H> {
403        &self.header
404    }
405
406    /// Gets token claims.
407    pub fn claims(&self) -> &Claims<T> {
408        &self.claims
409    }
410
411    /// Splits the `Token` into the respective `Header` and `Claims` while consuming it.
412    pub fn into_parts(self) -> (Header<H>, Claims<T>) {
413        (self.header, self.claims)
414    }
415}
416
417/// `Token` together with the validated token signature.
418///
419/// # Examples
420///
421/// ```
422/// # use jwt_compact::{alg::{Hs256, Hs256Key, Hs256Signature}, prelude::*};
423/// # use chrono::Duration;
424/// # use serde::{Deserialize, Serialize};
425/// #
426/// #[derive(Serialize, Deserialize)]
427/// struct MyClaims {
428///     // Custom claims in the token...
429/// }
430///
431/// # fn main() -> anyhow::Result<()> {
432/// # let key = Hs256Key::new(b"super_secret_key");
433/// # let claims = Claims::new(MyClaims {})
434/// #     .set_duration_and_issuance(&TimeOptions::default(), Duration::days(7));
435/// let token_string: String = // token from an external source
436/// #   Hs256.token(&Header::empty(), &claims, &key)?;
437/// let token = UntrustedToken::new(&token_string)?;
438/// let signed = Hs256.validator::<MyClaims>(&key)
439///     .validate_for_signed_token(&token)?;
440///
441/// // `signature` is strongly typed.
442/// let signature: Hs256Signature = signed.signature;
443/// // Token itself is available via `token` field.
444/// let claims = signed.token.claims();
445/// claims.validate_expiration(&TimeOptions::default())?;
446/// // Process the claims...
447/// # Ok(())
448/// # } // end main()
449/// ```
450#[non_exhaustive]
451pub struct SignedToken<A: Algorithm + ?Sized, T, H = Empty> {
452    /// Token signature.
453    pub signature: A::Signature,
454    /// Verified token.
455    pub token: Token<T, H>,
456}
457
458impl<A, T, H> fmt::Debug for SignedToken<A, T, H>
459where
460    A: Algorithm,
461    A::Signature: fmt::Debug,
462    T: fmt::Debug,
463    H: fmt::Debug,
464{
465    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
466        formatter
467            .debug_struct("SignedToken")
468            .field("token", &self.token)
469            .field("signature", &self.signature)
470            .finish()
471    }
472}
473
474impl<A, T, H> Clone for SignedToken<A, T, H>
475where
476    A: Algorithm,
477    A::Signature: Clone,
478    T: Clone,
479    H: Clone,
480{
481    fn clone(&self) -> Self {
482        Self {
483            signature: self.signature.clone(),
484            token: self.token.clone(),
485        }
486    }
487}
488
489impl<'a, H: DeserializeOwned> TryFrom<&'a str> for UntrustedToken<'a, H> {
490    type Error = ParseError;
491
492    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
493        let token_parts: Vec<_> = s.splitn(4, '.').collect();
494        match &token_parts[..] {
495            [header, claims, signature] => {
496                let header = Base64UrlUnpadded::decode_vec(header)
497                    .map_err(|_| ParseError::InvalidBase64Encoding)?;
498                let serialized_claims = Base64UrlUnpadded::decode_vec(claims)
499                    .map_err(|_| ParseError::InvalidBase64Encoding)?;
500
501                let mut decoded_signature = smallvec![0; 3 * (signature.len() + 3) / 4];
502                let signature_len =
503                    Base64UrlUnpadded::decode(signature, &mut decoded_signature[..])
504                        .map_err(|_| ParseError::InvalidBase64Encoding)?
505                        .len();
506                decoded_signature.truncate(signature_len);
507
508                let header: CompleteHeader<_> =
509                    serde_json::from_slice(&header).map_err(ParseError::MalformedHeader)?;
510                let content_type = match header.content_type {
511                    None => ContentType::Json,
512                    Some(s) if s.eq_ignore_ascii_case("json") => ContentType::Json,
513                    #[cfg(feature = "ciborium")]
514                    Some(s) if s.eq_ignore_ascii_case("cbor") => ContentType::Cbor,
515                    Some(s) => return Err(ParseError::UnsupportedContentType(s)),
516                };
517                let signed_data = s.rsplit_once('.').unwrap().0.as_bytes();
518                Ok(Self {
519                    signed_data: Cow::Borrowed(signed_data),
520                    header: header.inner,
521                    algorithm: header.algorithm.into_owned(),
522                    content_type,
523                    serialized_claims,
524                    signature: decoded_signature,
525                })
526            }
527            _ => Err(ParseError::InvalidTokenStructure),
528        }
529    }
530}
531
532impl<'a> UntrustedToken<'a> {
533    /// Creates an untrusted token from a string. This is a shortcut for calling the [`TryFrom`]
534    /// conversion.
535    pub fn new<S: AsRef<str> + ?Sized>(s: &'a S) -> Result<Self, ParseError> {
536        Self::try_from(s.as_ref())
537    }
538}
539
540impl<H> UntrustedToken<'_, H> {
541    /// Converts this token to an owned form.
542    pub fn into_owned(self) -> UntrustedToken<'static, H> {
543        UntrustedToken {
544            signed_data: Cow::Owned(self.signed_data.into_owned()),
545            header: self.header,
546            algorithm: self.algorithm,
547            content_type: self.content_type,
548            serialized_claims: self.serialized_claims,
549            signature: self.signature,
550        }
551    }
552
553    /// Gets the token header.
554    pub fn header(&self) -> &Header<H> {
555        &self.header
556    }
557
558    /// Gets the integrity algorithm used to secure the token.
559    pub fn algorithm(&self) -> &str {
560        &self.algorithm
561    }
562
563    /// Returns signature bytes from the token. These bytes are **not** guaranteed to form a valid
564    /// signature.
565    pub fn signature_bytes(&self) -> &[u8] {
566        &self.signature
567    }
568
569    /// Deserializes claims from this token without checking token integrity. The resulting
570    /// claims are thus **not** guaranteed to be valid.
571    pub fn deserialize_claims_unchecked<T>(&self) -> Result<Claims<T>, ValidationError>
572    where
573        T: DeserializeOwned,
574    {
575        match self.content_type {
576            ContentType::Json => serde_json::from_slice(&self.serialized_claims)
577                .map_err(ValidationError::MalformedClaims),
578
579            #[cfg(feature = "ciborium")]
580            ContentType::Cbor => {
581                ciborium::from_reader(&self.serialized_claims[..]).map_err(|err| {
582                    ValidationError::MalformedCborClaims(match err {
583                        CborDeError::Io(_) => CborDeError::Io(anyhow::anyhow!(
584                            "unexpected EOF in CBOR-serialized claims"
585                        )),
586                        CborDeError::Syntax(offset) => CborDeError::Syntax(offset),
587                        CborDeError::Semantic(offset, description) => {
588                            CborDeError::Semantic(offset, description)
589                        }
590                        CborDeError::RecursionLimitExceeded => CborDeError::RecursionLimitExceeded,
591                    })
592                })
593            }
594        }
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use alloc::{borrow::ToOwned, string::ToString};
601
602    use assert_matches::assert_matches;
603    use base64ct::{Base64UrlUnpadded, Encoding};
604
605    use super::*;
606    use crate::{
607        AlgorithmExt, Empty,
608        alg::{Hs256, Hs256Key},
609    };
610
611    type Obj = serde_json::Map<String, serde_json::Value>;
612
613    const HS256_TOKEN: &str = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.\
614                               eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt\
615                               cGxlLmNvbS9pc19yb290Ijp0cnVlfQ.\
616                               dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
617    const HS256_KEY: &str = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75\
618                             aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow";
619
620    #[test]
621    fn invalid_token_structure() {
622        let mangled_str = HS256_TOKEN.replace('.', "");
623        assert_matches!(
624            UntrustedToken::new(&mangled_str).unwrap_err(),
625            ParseError::InvalidTokenStructure
626        );
627
628        let mut mangled_str = HS256_TOKEN.to_owned();
629        let signature_start = mangled_str.rfind('.').unwrap();
630        mangled_str.truncate(signature_start);
631        assert_matches!(
632            UntrustedToken::new(&mangled_str).unwrap_err(),
633            ParseError::InvalidTokenStructure
634        );
635
636        let mut mangled_str = HS256_TOKEN.to_owned();
637        mangled_str.push('.');
638        assert_matches!(
639            UntrustedToken::new(&mangled_str).unwrap_err(),
640            ParseError::InvalidTokenStructure
641        );
642    }
643
644    #[test]
645    fn base64_error_during_parsing() {
646        let mangled_str = HS256_TOKEN.replace('0', "+");
647        assert_matches!(
648            UntrustedToken::new(&mangled_str).unwrap_err(),
649            ParseError::InvalidBase64Encoding
650        );
651    }
652
653    #[test]
654    fn base64_padding_error_during_parsing() {
655        let mut mangled_str = HS256_TOKEN.to_owned();
656        mangled_str.pop();
657        mangled_str.push('_'); // leads to non-zero padding for the last encoded byte
658        assert_matches!(
659            UntrustedToken::new(&mangled_str).unwrap_err(),
660            ParseError::InvalidBase64Encoding
661        );
662    }
663
664    #[test]
665    fn header_fields_are_not_serialized_if_not_present() {
666        let header = Header::empty();
667        let json = serde_json::to_string(&header).unwrap();
668        assert_eq!(json, "{}");
669    }
670
671    #[test]
672    fn header_with_x5t_field() {
673        let header = r#"{"alg":"HS256","x5t":"lDpwLQbzRZmu4fjajvn3KWAx1pk"}"#;
674        let header: CompleteHeader<Header<Empty>> = serde_json::from_str(header).unwrap();
675        let thumbprint = header.inner.certificate_sha1_thumbprint.as_ref().unwrap();
676        let Thumbprint::Bytes(thumbprint) = thumbprint else {
677            unreachable!();
678        };
679
680        assert_eq!(thumbprint[0], 0x94);
681        assert_eq!(thumbprint[19], 0x99);
682
683        let json = serde_json::to_value(header).unwrap();
684        assert_eq!(
685            json,
686            serde_json::json!({
687                "alg": "HS256",
688                "x5t": "lDpwLQbzRZmu4fjajvn3KWAx1pk",
689            })
690        );
691    }
692
693    #[test]
694    fn header_with_padded_x5t_field() {
695        let header = r#"{"alg":"HS256","x5t":"lDpwLQbzRZmu4fjajvn3KWAx1pk=="}"#;
696        let header: CompleteHeader<Header<Empty>> = serde_json::from_str(header).unwrap();
697        let thumbprint = header.inner.certificate_sha1_thumbprint.as_ref().unwrap();
698        let Thumbprint::Bytes(thumbprint) = thumbprint else {
699            unreachable!()
700        };
701
702        assert_eq!(thumbprint[0], 0x94);
703        assert_eq!(thumbprint[19], 0x99);
704    }
705
706    #[test]
707    fn header_with_hex_x5t_field() {
708        let header =
709            r#"{"alg":"HS256","x5t":"NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg"}"#;
710        let header: CompleteHeader<Header<Empty>> = serde_json::from_str(header).unwrap();
711        let thumbprint = header.inner.certificate_sha1_thumbprint.as_ref().unwrap();
712        let Thumbprint::String(thumbprint) = thumbprint else {
713            unreachable!()
714        };
715
716        assert_eq!(thumbprint, "65AF6909B1B0758E06C6E048C46002B5C695E36B");
717
718        let json = serde_json::to_value(header).unwrap();
719        assert_eq!(
720            json,
721            serde_json::json!({
722                "alg": "HS256",
723                "x5t": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg",
724            })
725        );
726    }
727
728    #[test]
729    fn header_with_padded_hex_x5t_field() {
730        let header =
731            r#"{"alg":"HS256","x5t":"NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg=="}"#;
732        let header: CompleteHeader<Header<Empty>> = serde_json::from_str(header).unwrap();
733        let thumbprint = header.inner.certificate_sha1_thumbprint.as_ref().unwrap();
734        let Thumbprint::String(thumbprint) = thumbprint else {
735            unreachable!()
736        };
737
738        assert_eq!(thumbprint, "65AF6909B1B0758E06C6E048C46002B5C695E36B");
739    }
740
741    #[test]
742    fn header_with_overly_short_x5t_field() {
743        let header = r#"{"alg":"HS256","x5t":"aGk="}"#;
744        let err = serde_json::from_str::<CompleteHeader<Header<Empty>>>(header).unwrap_err();
745        let err = err.to_string();
746        assert!(
747            err.contains("thumbprint must contain at least 20 bytes"),
748            "{err}"
749        );
750    }
751
752    #[test]
753    fn header_with_non_base64_x5t_field() {
754        let headers = [
755            r#"{"alg":"HS256","x5t":"lDpwLQbzRZmu4fjajvn3KWAx1p?"}"#,
756            r#"{"alg":"HS256","x5t":"NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk!RTM2Qg"}"#,
757        ];
758        for header in headers {
759            let err = serde_json::from_str::<CompleteHeader<Header<Empty>>>(header).unwrap_err();
760            let err = err.to_string();
761            assert!(err.contains("Base64"), "{err}");
762        }
763    }
764
765    #[test]
766    fn header_with_x5t_sha256_field() {
767        let header = r#"{"alg":"HS256","x5t#S256":"MV9b23bQeMQ7isAGTkoBZGErH853yGk0W_yUx1iU7dM"}"#;
768        let header: CompleteHeader<Header<Empty>> = serde_json::from_str(header).unwrap();
769        let thumbprint = header.inner.certificate_thumbprint.as_ref().unwrap();
770        let Thumbprint::Bytes(thumbprint) = thumbprint else {
771            unreachable!()
772        };
773
774        assert_eq!(thumbprint[0], 0x31);
775        assert_eq!(thumbprint[31], 0xd3);
776
777        let json = serde_json::to_value(header).unwrap();
778        assert_eq!(
779            json,
780            serde_json::json!({
781                "alg": "HS256",
782                "x5t#S256": "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W_yUx1iU7dM",
783            })
784        );
785    }
786
787    #[test]
788    fn malformed_header() {
789        let mangled_headers = [
790            // Missing closing brace
791            r#"{"alg":"HS256""#,
792            // Missing necessary `alg` field
793            "{}",
794            // `alg` field is not a string
795            r#"{"alg":5}"#,
796            r#"{"alg":[1,"foo"]}"#,
797            r#"{"alg":false}"#,
798            // Duplicate `alg` field
799            r#"{"alg":"HS256","alg":"none"}"#,
800            // Invalid thumbprint fields
801            r#"{"alg":"HS256","x5t":"lDpwLQbzRZmu4fjajvn3KWAx1p"}"#,
802            r#"{"alg":"HS256","x5t":["lDpwLQbzRZmu4fjajvn3KWAx1pk"]}"#,
803            r#"{"alg":"HS256","x5t":"lDpwLQbzRZmu4fjajvn3KWAx1 k"}"#,
804            r#"{"alg":"HS256","x5t":"lDpwLQbzRZmu4fjajvn3KWAx1pk==="}"#,
805            r#"{"alg":"HS256","x5t":"lDpwLQbzRZmu4fjajvn3KWAx1pkk"}"#,
806            r#"{"alg":"HS256","x5t":"MV9b23bQeMQ7isAGTkoBZGErH853yGk0W_yUx1iU7dM"}"#,
807            r#"{"alg":"HS256","x5t#S256":"lDpwLQbzRZmu4fjajvn3KWAx1pk"}"#,
808        ];
809
810        for mangled_header in &mangled_headers {
811            let mangled_header = Base64UrlUnpadded::encode_string(mangled_header.as_bytes());
812            let mut mangled_str = HS256_TOKEN.to_owned();
813            mangled_str.replace_range(..mangled_str.find('.').unwrap(), &mangled_header);
814            assert_matches!(
815                UntrustedToken::new(&mangled_str).unwrap_err(),
816                ParseError::MalformedHeader(_)
817            );
818        }
819    }
820
821    #[test]
822    fn unsupported_content_type() {
823        let mangled_header = br#"{"alg":"HS256","cty":"txt"}"#;
824        let mangled_header = Base64UrlUnpadded::encode_string(mangled_header);
825        let mut mangled_str = HS256_TOKEN.to_owned();
826        mangled_str.replace_range(..mangled_str.find('.').unwrap(), &mangled_header);
827        assert_matches!(
828            UntrustedToken::new(&mangled_str).unwrap_err(),
829            ParseError::UnsupportedContentType(s) if s == "txt"
830        );
831    }
832
833    #[test]
834    fn extracting_custom_header_fields() {
835        let header = r#"{"alg":"HS256","custom":[1,"field"],"x5t":"lDpwLQbzRZmu4fjajvn3KWAx1pk"}"#;
836        let header: CompleteHeader<Header<Obj>> = serde_json::from_str(header).unwrap();
837        assert_eq!(header.algorithm, "HS256");
838        assert!(header.inner.certificate_sha1_thumbprint.is_some());
839        assert_eq!(header.inner.other_fields.len(), 1);
840        assert!(header.inner.other_fields["custom"].is_array());
841    }
842
843    #[test]
844    fn malformed_json_claims() {
845        let malformed_claims = [
846            // Missing closing brace
847            r#"{"exp":1500000000"#,
848            // `exp` claim is not a number
849            r#"{"exp":"1500000000"}"#,
850            r#"{"exp":false}"#,
851            // Duplicate `exp` claim
852            r#"{"exp":1500000000,"nbf":1400000000,"exp":1510000000}"#,
853            // Too large `exp` value
854            r#"{"exp":1500000000000000000000000000000000}"#,
855        ];
856
857        let claims_start = HS256_TOKEN.find('.').unwrap() + 1;
858        let claims_end = HS256_TOKEN.rfind('.').unwrap();
859        let key = Base64UrlUnpadded::decode_vec(HS256_KEY).unwrap();
860        let key = Hs256Key::new(key);
861
862        for claims in &malformed_claims {
863            let encoded_claims = Base64UrlUnpadded::encode_string(claims.as_bytes());
864            let mut mangled_str = HS256_TOKEN.to_owned();
865            mangled_str.replace_range(claims_start..claims_end, &encoded_claims);
866            let token = UntrustedToken::new(&mangled_str).unwrap();
867            assert_matches!(
868                Hs256.validator::<Obj>(&key).validate(&token).unwrap_err(),
869                ValidationError::MalformedClaims(_),
870                "Failing claims: {claims}"
871            );
872        }
873    }
874
875    fn test_invalid_signature_len(mangled_str: &str, actual_len: usize) {
876        let token = UntrustedToken::new(&mangled_str).unwrap();
877        let key = Base64UrlUnpadded::decode_vec(HS256_KEY).unwrap();
878        let key = Hs256Key::new(key);
879
880        let err = Hs256.validator::<Empty>(&key).validate(&token).unwrap_err();
881        assert_matches!(
882            err,
883            ValidationError::InvalidSignatureLen { actual, expected: 32 }
884                if actual == actual_len
885        );
886    }
887
888    #[test]
889    fn short_signature_error() {
890        test_invalid_signature_len(&HS256_TOKEN[..HS256_TOKEN.len() - 3], 30);
891    }
892
893    #[test]
894    fn long_signature_error() {
895        let mut mangled_string = HS256_TOKEN.to_owned();
896        mangled_string.push('a');
897        test_invalid_signature_len(&mangled_string, 33);
898    }
899}