jwt_compact/
token.rs

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