Skip to main content

jwt_compact_preview/
lib.rs

1//! Minimalistic [JSON web token (JWT)][JWT] implementation with focus on type safety
2//! and secure cryptographic primitives.
3//!
4//! # Design choices
5//!
6//! - JWT signature algorithms (i.e., cryptographic algorithms providing JWT integrity)
7//!   are expressed via the [`Algorithm`] trait, which uses fully typed keys and signatures.
8//! - [JWT header] is represented by the [`Header`] struct. Notably, `Header` does not
9//!   expose the [`alg` field].
10//!   Instead, `alg` is filled automatically during token creation, and is compared to the
11//!   expected value during verification. (If you do not know the JWT signature algorithm during
12//!   verification, you're doing something wrong.) This eliminates the possibility
13//!   of [algorithm switching attacks][switching].
14//!
15//! # Additional features
16//!
17//! - The crate supports more compact [CBOR] encoding of the claims. The compactly encoded JWTs
18//!   have [`cty` field] (content type) in their header set to `"CBOR"`.
19//! - The crate supports `EdDSA` algorithm with the Ed25519 elliptic curve, and `ES256K` algorithm
20//!   with the secp256k1 elliptic curve.
21//!
22//! ## Supported algorithms
23//!
24//! | Algorithm(s) | Feature | Description |
25//! |--------------|---------|-------------|
26//! | `HS256`, `HS384`, `HS512` | - | Uses pure Rust [`sha2`] crate |
27//! | `EdDSA` (Ed25519) | [`exonum-crypto`] | [`libsodium`] binding. Enabled by default |
28//! | `EdDSA` (Ed25519) | [`ed25519-dalek`] | Pure Rust implementation |
29//! | `EdDSA` (Ed25519) | [`ed25519-compact`] | Compact pure Rust implementation, WASM-compatible |
30//! | `ES256K` | [`secp256k1`] | Binding for [`libsecp256k1`] |
31//! | `RS*`, `PS*` (RSA) | [`rsa`] | Uses pure Rust [`rsa`] crate with blinding |
32//!
33//! Standard`ES*` algorithm is not (yet?) implemented. The main reason (besides
34//! laziness and non-friendly APIs in the relevant crypto backends) is:
35//!
36//! - Elliptic curves in `ES*` algs use a maybe-something-up-my-sleeve generation procedure
37//!   and thus may be backdoored
38//!
39//! `EdDSA` and `ES256K` algorithms are non-standard. They both work with elliptic curves
40//! (Curve25519 and secp256k1; both are widely used in crypto community and believed to be
41//! securely generated). These algs have 128-bit security, making them an alternative
42//! to `ES256`.
43//!
44//! [JWT]: https://jwt.io/
45//! [switching]: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
46//! [JWT header]: https://tools.ietf.org/html/rfc7519#section-5
47//! [`alg` field]: https://tools.ietf.org/html/rfc7515#section-4.1.1
48//! [`cty` field]: https://tools.ietf.org/html/rfc7515#section-4.1.10
49//! [CBOR]: https://tools.ietf.org/html/rfc7049
50//! [`sha2`]: https://docs.rs/sha2/
51//! [`libsodium`]: https://download.libsodium.org/doc/
52//! [`exonum-crypto`]: https://docs.rs/exonum-crypto/
53//! [`ed25519-dalek`]: https://doc.dalek.rs/ed25519_dalek/
54//! [`ed25519-compact`]: https://crates.io/crates/ed25519-compact
55//! [`secp256k1`]: https://docs.rs/secp256k1/
56//! [`libsecp256k1`]: https://github.com/bitcoin-core/secp256k1
57//! [`Header`]: struct.Header.html
58//! [`Algorithm`]: trait.Algorithm.html
59//!
60//! # Examples
61//!
62//! Basic JWT lifecycle:
63//!
64//! ```
65//! use chrono::{Duration, Utc};
66//! use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
67//! use serde::{Serialize, Deserialize};
68//! use std::convert::TryFrom;
69//!
70//! /// Custom claims encoded in the token.
71//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
72//! struct CustomClaims {
73//!     /// `sub` is a standard claim which denotes claim subject:
74//!     /// https://tools.ietf.org/html/rfc7519#section-4.1.2
75//!     #[serde(rename = "sub")]
76//!     subject: String,
77//! }
78//!
79//! # fn main() -> anyhow::Result<()> {
80//! // Create a symmetric HMAC key, which will be used both to create and verify tokens.
81//! let key = Hs256Key::from(b"super_secret_key_donut_steel" as &[_]);
82//! // Create a token.
83//! let header = Header {
84//!     key_id: Some("my-key".to_owned()),
85//!     ..Default::default()
86//! };
87//! let claims = Claims::new(CustomClaims { subject: "alice".to_owned() })
88//!     .set_duration_and_issuance(Duration::days(7))
89//!     .set_not_before(Utc::now() - Duration::hours(1));
90//! let token_string = Hs256.token(header, &claims, &key)?;
91//! println!("token: {}", token_string);
92//!
93//! // Parse the token.
94//! let token = UntrustedToken::try_from(token_string.as_str())?;
95//! // Before verifying the token, we might find the key which has signed the token
96//! // using the `Header.key_id` field.
97//! assert_eq!(token.header().key_id, Some("my-key".to_owned()));
98//! // Validate the token integrity.
99//! let token: Token<CustomClaims> = Hs256.validate_integrity(&token, &key)?;
100//! // Validate additional conditions.
101//! token
102//!     .claims()
103//!     .validate_expiration(TimeOptions::default())?
104//!     .validate_maturity(TimeOptions::default())?;
105//! // Now, we can extract information from the token (e.g., its subject).
106//! let subject = &token.claims().custom.subject;
107//! assert_eq!(subject, "alice");
108//! # Ok(())
109//! # } // end main()
110//! ```
111//!
112//! ## Compact JWT
113//!
114//! ```
115//! # use chrono::Duration;
116//! # use hex_buffer_serde::{Hex as _, HexForm};
117//! # use jwt_compact::{prelude::*, alg::{Hs256, Hs256Key}};
118//! # use serde::{Serialize, Deserialize};
119//! # use std::convert::TryFrom;
120//! /// Custom claims encoded in the token.
121//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
122//! struct CustomClaims {
123//!     /// `sub` is a standard claim which denotes claim subject:
124//!     ///     https://tools.ietf.org/html/rfc7519#section-4.1.2
125//!     /// The custom serializer we use allows to efficiently
126//!     /// encode the subject in CBOR.
127//!     #[serde(rename = "sub", with = "HexForm")]
128//!     subject: [u8; 32],
129//! }
130//!
131//! # fn main() -> anyhow::Result<()> {
132//! let key = Hs256Key::from(b"super_secret_key_donut_steel" as &[_]);
133//! let claims = Claims::new(CustomClaims { subject: [111; 32] })
134//!     .set_duration_and_issuance(Duration::days(7));
135//! let token = Hs256.token(Header::default(), &claims, &key)?;
136//! println!("token: {}", token);
137//! let compact_token = Hs256.compact_token(Header::default(), &claims, &key)?;
138//! println!("compact token: {}", compact_token);
139//! // The compact token should be ~40 chars shorter.
140//!
141//! // Parse the compact token.
142//! let token = UntrustedToken::try_from(compact_token.as_str())?;
143//! let token: Token<CustomClaims> = Hs256.validate_integrity(&token, &key)?;
144//! token.claims().validate_expiration(TimeOptions::default())?;
145//! // Now, we can extract information from the token (e.g., its subject).
146//! assert_eq!(token.claims().custom.subject, [111; 32]);
147//! # Ok(())
148//! # } // end main()
149//! ```
150
151#![deny(missing_debug_implementations, missing_docs, bare_trait_objects)]
152
153use serde::{de::DeserializeOwned, Deserialize, Serialize};
154use smallvec::{smallvec, SmallVec};
155
156use std::{borrow::Cow, convert::TryFrom, fmt};
157
158pub mod alg;
159mod claims;
160mod error;
161
162pub use crate::{
163    claims::{Claims, Empty, TimeOptions},
164    error::{CreationError, ParseError, ValidationError},
165};
166
167/// Prelude to neatly import all necessary stuff from the crate.
168pub mod prelude {
169    pub use crate::{AlgorithmExt as _, Claims, Header, TimeOptions, Token, UntrustedToken};
170}
171
172/// Maximum "reasonable" signature size in bytes.
173const SIGNATURE_SIZE: usize = 128;
174
175/// Signature for a certain JWT signing `Algorithm`.
176///
177/// We require that signature can be restored from a byte slice,
178/// and can be represented as a byte slice.
179pub trait AlgorithmSignature: Sized {
180    /// Attempts to restore a signature from a byte slice. This method may fail
181    /// if the slice is malformed (e.g., has a wrong length).
182    fn try_from_slice(slice: &[u8]) -> anyhow::Result<Self>;
183
184    /// Represents this signature as bytes.
185    fn as_bytes(&self) -> Cow<'_, [u8]>;
186}
187
188/// JWT signing algorithm.
189pub trait Algorithm {
190    /// Key used when issuing new tokens.
191    type SigningKey;
192    /// Key used when verifying tokens. May coincide with `SigningKey` for symmetric
193    /// algorithms (e.g., `HS*`).
194    type VerifyingKey;
195    /// Signature produced by the algorithm.
196    type Signature: AlgorithmSignature;
197
198    /// Returns the name of this algorithm, as mentioned in the `alg` field of the JWT header.
199    fn name(&self) -> Cow<'static, str>;
200
201    /// Signs a `message` with the `signing_key`.
202    fn sign(&self, signing_key: &Self::SigningKey, message: &[u8]) -> Self::Signature;
203
204    /// Verifies the `message` against the `signature` and `verifying_key`.
205    fn verify_signature(
206        &self,
207        signature: &Self::Signature,
208        verifying_key: &Self::VerifyingKey,
209        message: &[u8],
210    ) -> bool;
211}
212
213/// Algorithm that uses a custom name when creating and validating tokens.
214///
215/// # Examples
216///
217/// ```
218/// use jwt_compact::{alg::{Hs256, Hs256Key}, prelude::*, Empty, Renamed};
219/// # use std::convert::TryFrom;
220///
221/// let alg = Renamed::new(Hs256, "HS2");
222/// let key = Hs256Key::from(b"super_secret_key_donut_steel" as &[_]);
223/// let token_string = alg.token(Header::default(), &Claims::empty(), &key).unwrap();
224///
225/// let token = UntrustedToken::try_from(token_string.as_str()).unwrap();
226/// assert_eq!(token.algorithm(), "HS2");
227/// // Note that the created token cannot be verified against the original algorithm
228/// // since the algorithm name recorded in the token header doesn't match.
229/// assert!(Hs256.validate_integrity::<Empty>(&token, &key).is_err());
230///
231/// // ...but the modified alg is working as expected.
232/// assert!(alg.validate_integrity::<Empty>(&token, &key).is_ok());
233/// ```
234#[derive(Debug, Clone, Copy)]
235pub struct Renamed<A> {
236    inner: A,
237    name: &'static str,
238}
239
240impl<A: Algorithm> Renamed<A> {
241    /// Creates a renamed algorithm.
242    pub fn new(algorithm: A, new_name: &'static str) -> Self {
243        Self {
244            inner: algorithm,
245            name: new_name,
246        }
247    }
248}
249
250impl<A: Algorithm> Algorithm for Renamed<A> {
251    type SigningKey = A::SigningKey;
252    type VerifyingKey = A::VerifyingKey;
253    type Signature = A::Signature;
254
255    fn name(&self) -> Cow<'static, str> {
256        Cow::Borrowed(self.name)
257    }
258
259    fn sign(&self, signing_key: &Self::SigningKey, message: &[u8]) -> Self::Signature {
260        self.inner.sign(signing_key, message)
261    }
262
263    fn verify_signature(
264        &self,
265        signature: &Self::Signature,
266        verifying_key: &Self::VerifyingKey,
267        message: &[u8],
268    ) -> bool {
269        self.inner
270            .verify_signature(signature, verifying_key, message)
271    }
272}
273
274/// Automatically implemented extensions of the `Algorithm` trait.
275pub trait AlgorithmExt: Algorithm {
276    /// Creates a new token and serializes it to string.
277    fn token<T>(
278        &self,
279        header: Header,
280        claims: &Claims<T>,
281        signing_key: &Self::SigningKey,
282    ) -> Result<String, CreationError>
283    where
284        T: Serialize;
285
286    /// Creates a new token with CBOR-encoded claims and serializes it to string.
287    fn compact_token<T>(
288        &self,
289        header: Header,
290        claims: &Claims<T>,
291        signing_key: &Self::SigningKey,
292    ) -> Result<String, CreationError>
293    where
294        T: Serialize;
295
296    /// Validates the token integrity against the provided `verifying_key`.
297    fn validate_integrity<T>(
298        &self,
299        token: &UntrustedToken<'_>,
300        verifying_key: &Self::VerifyingKey,
301    ) -> Result<Token<T>, ValidationError>
302    where
303        T: DeserializeOwned;
304
305    /// Validates the token integrity against the provided `verifying_key`.
306    ///
307    /// Unlike [`validate_integrity`](#tymethod.validate_integrity), this method retains more
308    /// information about the original token, in particular, its signature.
309    fn validate_for_signed_token<T>(
310        &self,
311        token: &UntrustedToken<'_>,
312        verifying_key: &Self::VerifyingKey,
313    ) -> Result<SignedToken<Self, T>, ValidationError>
314    where
315        T: DeserializeOwned;
316}
317
318impl<A: Algorithm> AlgorithmExt for A {
319    fn token<T>(
320        &self,
321        header: Header,
322        claims: &Claims<T>,
323        signing_key: &Self::SigningKey,
324    ) -> Result<String, CreationError>
325    where
326        T: Serialize,
327    {
328        let complete_header = CompleteHeader {
329            algorithm: self.name(),
330            content_type: None,
331            inner: header,
332        };
333        let header = serde_json::to_string(&complete_header).map_err(CreationError::Header)?;
334        let mut buffer = base64::encode_config(&header, base64::URL_SAFE_NO_PAD);
335
336        buffer.push('.');
337        let claims = serde_json::to_string(claims).map_err(CreationError::Claims)?;
338        base64::encode_config_buf(&claims, base64::URL_SAFE_NO_PAD, &mut buffer);
339
340        let signature = self.sign(signing_key, buffer.as_bytes());
341        buffer.push('.');
342        base64::encode_config_buf(
343            signature.as_bytes().as_ref(),
344            base64::URL_SAFE_NO_PAD,
345            &mut buffer,
346        );
347
348        Ok(buffer)
349    }
350
351    fn compact_token<T>(
352        &self,
353        header: Header,
354        claims: &Claims<T>,
355        signing_key: &Self::SigningKey,
356    ) -> Result<String, CreationError>
357    where
358        T: Serialize,
359    {
360        let complete_header = CompleteHeader {
361            algorithm: self.name(),
362            content_type: Some("CBOR".to_owned()),
363            inner: header,
364        };
365        let header = serde_json::to_string(&complete_header).map_err(CreationError::Header)?;
366        let mut buffer = base64::encode_config(&header, base64::URL_SAFE_NO_PAD);
367
368        buffer.push('.');
369        let claims = serde_cbor::to_vec(claims).map_err(CreationError::CborClaims)?;
370        base64::encode_config_buf(&claims, base64::URL_SAFE_NO_PAD, &mut buffer);
371
372        let signature = self.sign(signing_key, buffer.as_bytes());
373        buffer.push('.');
374        base64::encode_config_buf(
375            signature.as_bytes().as_ref(),
376            base64::URL_SAFE_NO_PAD,
377            &mut buffer,
378        );
379
380        Ok(buffer)
381    }
382
383    fn validate_integrity<T>(
384        &self,
385        token: &UntrustedToken<'_>,
386        verifying_key: &Self::VerifyingKey,
387    ) -> Result<Token<T>, ValidationError>
388    where
389        T: DeserializeOwned,
390    {
391        self.validate_for_signed_token(token, verifying_key)
392            .map(|wrapper| wrapper.token)
393    }
394
395    fn validate_for_signed_token<T>(
396        &self,
397        token: &UntrustedToken<'_>,
398        verifying_key: &Self::VerifyingKey,
399    ) -> Result<SignedToken<Self, T>, ValidationError>
400    where
401        T: DeserializeOwned,
402    {
403        if self.name() != token.algorithm {
404            return Err(ValidationError::AlgorithmMismatch);
405        }
406
407        let signature = Self::Signature::try_from_slice(&token.signature[..])
408            .map_err(ValidationError::MalformedSignature)?;
409        // We assume that parsing claims is less computationally demanding than
410        // validating a signature.
411        let claims: Claims<T> = match token.content_type {
412            ContentType::Json => serde_json::from_slice(&token.serialized_claims)
413                .map_err(ValidationError::MalformedClaims)?,
414            ContentType::Cbor => serde_cbor::from_slice(&token.serialized_claims)
415                .map_err(ValidationError::MalformedCborClaims)?,
416        };
417        if !self.verify_signature(&signature, verifying_key, token.signed_data) {
418            return Err(ValidationError::InvalidSignature);
419        }
420
421        Ok(SignedToken {
422            signature,
423            token: Token {
424                header: token.header.clone(),
425                claims,
426            },
427        })
428    }
429}
430
431/// JWT header.
432///
433/// See [RFC 7515](https://tools.ietf.org/html/rfc7515#section-4.1) for the description
434/// of the fields. The purpose of all fields except `signature_type` is to determine
435/// the verifying key. Since these values will be provided by the adversary in the case of
436/// an attack, they require additional verification (e.g., a provided certificate might
437/// be checked against the list of "acceptable" certificate authorities).
438#[derive(Debug, Clone, Default, Serialize, Deserialize)]
439pub struct Header {
440    /// URL of the JSON Web Key Set containing the key that has signed the token.
441    /// This field is renamed to `jku` for serialization.
442    #[serde(rename = "jku", default, skip_serializing_if = "Option::is_none")]
443    pub key_set_url: Option<String>,
444
445    /// Identifier of the key that has signed the token. This field is renamed to `kid`
446    /// for serialization.
447    #[serde(rename = "kid", default, skip_serializing_if = "Option::is_none")]
448    pub key_id: Option<String>,
449
450    /// URL of the X.509 certificate for the signing key. This field is renamed to `x5u`
451    /// for serialization.
452    #[serde(rename = "x5u", default, skip_serializing_if = "Option::is_none")]
453    pub certificate_url: Option<String>,
454
455    /// Thumbprint of the X.509 certificate for the signing key. This field is renamed to `x5t`
456    /// for serialization.
457    #[serde(rename = "x5t", default, skip_serializing_if = "Option::is_none")]
458    pub certificate_thumbprint: Option<String>,
459
460    /// Application-specific signature type. This field is renamed to `typ` for serialization.
461    #[serde(rename = "typ", default, skip_serializing_if = "Option::is_none")]
462    pub signature_type: Option<String>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
466struct CompleteHeader<'a> {
467    #[serde(rename = "alg")]
468    algorithm: Cow<'a, str>,
469
470    #[serde(rename = "cty", default, skip_serializing_if = "Option::is_none")]
471    content_type: Option<String>,
472
473    #[serde(flatten)]
474    inner: Header,
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478enum ContentType {
479    Json,
480    Cbor,
481}
482
483/// Parsed, but unvalidated token.
484#[derive(Debug, Clone)]
485pub struct UntrustedToken<'a> {
486    signed_data: &'a [u8],
487    header: Header,
488    algorithm: String,
489    content_type: ContentType,
490    serialized_claims: Vec<u8>,
491    signature: SmallVec<[u8; SIGNATURE_SIZE]>,
492}
493
494/// Token with validated integrity.
495///
496/// Claims encoded in the token can be verified by invoking [`Claims`] methods
497/// via [`claims()`] getter.
498///
499/// [`Claims`]: struct.Claims.html
500/// [`claims()`]: #fn.claims
501#[derive(Debug, Clone)]
502pub struct Token<T> {
503    header: Header,
504    claims: Claims<T>,
505}
506
507impl<T> Token<T> {
508    /// Gets token header.
509    pub fn header(&self) -> &Header {
510        &self.header
511    }
512
513    /// Gets token claims.
514    pub fn claims(&self) -> &Claims<T> {
515        &self.claims
516    }
517}
518
519/// `Token` together with the validated token signature.
520///
521/// # Examples
522///
523/// ```
524/// # use jwt_compact::{alg::{Hs256, Hs256Key}, prelude::*};
525/// # use chrono::Duration;
526/// # use hmac::crypto_mac::generic_array::{typenum, GenericArray};
527/// # use serde::{Deserialize, Serialize};
528/// # use std::convert::TryFrom;
529/// #
530/// #[derive(Serialize, Deserialize)]
531/// struct MyClaims {
532///     // Custom claims in the token...
533/// }
534///
535/// # fn main() -> anyhow::Result<()> {
536/// # let key = Hs256Key::from(b"super_secret_key" as &[_]);
537/// # let claims = Claims::new(MyClaims {}).set_duration_and_issuance(Duration::days(7));
538/// let token_string: String = // token from an external source
539/// #   Hs256.token(Header::default(), &claims, &key)?;
540/// let token = UntrustedToken::try_from(token_string.as_str())?;
541/// let signed = Hs256.validate_for_signed_token::<MyClaims>(&token, &key)?;
542///
543/// // `signature` is strongly typed.
544/// let array: GenericArray<u8, typenum::U32> = signed.signature.into_bytes();
545/// // Token itself is available via `token` field.
546/// let claims = signed.token.claims();
547/// claims.validate_expiration(TimeOptions::default())?;
548/// // Process the claims...
549/// # Ok(())
550/// # } // end main()
551/// ```
552#[non_exhaustive]
553pub struct SignedToken<A: Algorithm + ?Sized, T> {
554    /// Token signature.
555    pub signature: A::Signature,
556    /// Verified token.
557    pub token: Token<T>,
558}
559
560impl<A, T> fmt::Debug for SignedToken<A, T>
561where
562    A: Algorithm,
563    A::Signature: fmt::Debug,
564    T: fmt::Debug,
565{
566    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
567        formatter
568            .debug_struct("SignedToken")
569            .field("token", &self.token)
570            .field("signature", &self.signature)
571            .finish()
572    }
573}
574
575impl<A, T> Clone for SignedToken<A, T>
576where
577    A: Algorithm,
578    A::Signature: Clone,
579    T: Clone,
580{
581    fn clone(&self) -> Self {
582        Self {
583            signature: self.signature.clone(),
584            token: self.token.clone(),
585        }
586    }
587}
588
589impl<'a> TryFrom<&'a str> for UntrustedToken<'a> {
590    type Error = ParseError;
591
592    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
593        let token_parts: Vec<_> = s.splitn(4, '.').collect();
594        match &token_parts[..] {
595            [header, claims, signature] => {
596                let header = base64::decode_config(header, base64::URL_SAFE_NO_PAD)?;
597                let serialized_claims = base64::decode_config(claims, base64::URL_SAFE_NO_PAD)?;
598                let mut decoded_signature = smallvec![0; 3 * (signature.len() + 3) / 4];
599                let signature_len = base64::decode_config_slice(
600                    signature,
601                    base64::URL_SAFE_NO_PAD,
602                    &mut decoded_signature[..],
603                )?;
604                decoded_signature.truncate(signature_len);
605
606                let header: CompleteHeader<'_> =
607                    serde_json::from_slice(&header).map_err(ParseError::MalformedHeader)?;
608                let content_type = match header.content_type {
609                    None => ContentType::Json,
610                    Some(ref s) if s.eq_ignore_ascii_case("json") => ContentType::Json,
611                    Some(ref s) if s.eq_ignore_ascii_case("cbor") => ContentType::Cbor,
612                    Some(s) => return Err(ParseError::UnsupportedContentType(s)),
613                };
614
615                Ok(Self {
616                    signed_data: s.rsplitn(2, '.').nth(1).unwrap().as_bytes(),
617                    header: header.inner,
618                    algorithm: header.algorithm.into_owned(),
619                    content_type,
620                    serialized_claims,
621                    signature: decoded_signature,
622                })
623            }
624            _ => Err(ParseError::InvalidTokenStructure),
625        }
626    }
627}
628
629impl<'a> UntrustedToken<'a> {
630    /// Gets the token header.
631    pub fn header(&self) -> &Header {
632        &self.header
633    }
634
635    /// Gets the integrity algorithm used to secure the token.
636    pub fn algorithm(&self) -> &str {
637        &self.algorithm
638    }
639
640    /// Returns signature bytes from the token. These bytes are **not** guaranteed to form a valid
641    /// signature.
642    pub fn signature_bytes(&self) -> &[u8] {
643        &self.signature
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use crate::alg::*;
651    use assert_matches::assert_matches;
652
653    type Obj = serde_json::Map<String, serde_json::Value>;
654
655    const HS256_TOKEN: &str = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.\
656                               eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt\
657                               cGxlLmNvbS9pc19yb290Ijp0cnVlfQ.\
658                               dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
659    const HS256_KEY: &str = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75\
660                             aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow";
661
662    #[test]
663    fn invalid_token_structure() {
664        let mangled_str = HS256_TOKEN.replace('.', "");
665        assert_matches!(
666            UntrustedToken::try_from(mangled_str.as_str()).unwrap_err(),
667            ParseError::InvalidTokenStructure
668        );
669
670        let mut mangled_str = HS256_TOKEN.to_owned();
671        let signature_start = mangled_str.rfind('.').unwrap();
672        mangled_str.truncate(signature_start);
673        assert_matches!(
674            UntrustedToken::try_from(mangled_str.as_str()).unwrap_err(),
675            ParseError::InvalidTokenStructure
676        );
677
678        let mut mangled_str = HS256_TOKEN.to_owned();
679        mangled_str.push('.');
680        assert_matches!(
681            UntrustedToken::try_from(mangled_str.as_str()).unwrap_err(),
682            ParseError::InvalidTokenStructure
683        );
684    }
685
686    #[test]
687    fn base64_error_during_parsing() {
688        let mangled_str = HS256_TOKEN.replace('0', "+");
689        assert_matches!(
690            UntrustedToken::try_from(mangled_str.as_str()).unwrap_err(),
691            ParseError::Base64(_)
692        );
693
694        let mut mangled_str = HS256_TOKEN.to_owned();
695        mangled_str.truncate(mangled_str.len() - 1);
696        assert_matches!(
697            UntrustedToken::try_from(mangled_str.as_str()).unwrap_err(),
698            ParseError::Base64(_)
699        );
700    }
701
702    #[test]
703    fn malformed_header() {
704        let mangled_headers = [
705            // Missing closing brace
706            r#"{"alg":"HS256""#,
707            // Missing necessary `alg` field
708            "{}",
709            // `alg` field is not a string
710            r#"{"alg":5}"#,
711            r#"{"alg":[1,"foo"]}"#,
712            r#"{"alg":false}"#,
713            // Duplicate `alg` field
714            r#"{"alg":"HS256","alg":"none"}"#,
715        ];
716
717        for mangled_header in &mangled_headers {
718            let mangled_header = base64::encode_config(mangled_header, base64::URL_SAFE_NO_PAD);
719            let mut mangled_str = HS256_TOKEN.to_owned();
720            mangled_str.replace_range(..mangled_str.find('.').unwrap(), &mangled_header);
721            assert_matches!(
722                UntrustedToken::try_from(mangled_str.as_str()).unwrap_err(),
723                ParseError::MalformedHeader(_)
724            );
725        }
726    }
727
728    #[test]
729    fn unsupported_content_type() {
730        let mangled_header = r#"{"alg":"HS256","cty":"txt"}"#;
731        let mangled_header = base64::encode_config(mangled_header, base64::URL_SAFE_NO_PAD);
732        let mut mangled_str = HS256_TOKEN.to_owned();
733        mangled_str.replace_range(..mangled_str.find('.').unwrap(), &mangled_header);
734        assert_matches!(
735            UntrustedToken::try_from(mangled_str.as_str()).unwrap_err(),
736            ParseError::UnsupportedContentType(ref s) if s == "txt"
737        );
738    }
739
740    #[test]
741    fn malformed_json_claims() {
742        let malformed_claims = [
743            // Missing closing brace
744            r#"{"exp":1500000000"#,
745            // `exp` claim is not a number
746            r#"{"exp":"1500000000"}"#,
747            r#"{"exp":false}"#,
748            // Duplicate `exp` claim
749            r#"{"exp":1500000000,"nbf":1400000000,"exp":1510000000}"#,
750            // Too large `exp` value
751            r#"{"exp":1500000000000000000000000000000000}"#,
752        ];
753
754        let claims_start = HS256_TOKEN.find('.').unwrap() + 1;
755        let claims_end = HS256_TOKEN.rfind('.').unwrap();
756        let key = base64::decode_config(HS256_KEY, base64::URL_SAFE_NO_PAD).unwrap();
757        let key = Hs256Key::from(&*key);
758
759        for claims in &malformed_claims {
760            let encoded_claims = base64::encode_config(claims.as_bytes(), base64::URL_SAFE_NO_PAD);
761            let mut mangled_str = HS256_TOKEN.to_owned();
762            mangled_str.replace_range(claims_start..claims_end, &encoded_claims);
763            let token = UntrustedToken::try_from(mangled_str.as_str()).unwrap();
764            assert_matches!(
765                Hs256.validate_integrity::<Obj>(&token, &key).unwrap_err(),
766                ValidationError::MalformedClaims(_),
767                "Failing claims: {}",
768                claims
769            );
770        }
771    }
772}