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}