jsonwebtoken-hs256 0.1.0

Create and decode JWTs in a strongly typed way.
Documentation
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use ring::constant_time::verify_slices_are_equal;
use ring::hmac::{self, HMAC_SHA256};
use serde::de::DeserializeOwned;
use serde::de::{self, Visitor};
use serde::Deserializer;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt;
use std::marker::PhantomData;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum EncodeError {
    #[error("{0}")]
    Json(#[from] serde_json::Error),
}

#[derive(Debug, Error)]
pub enum DecodeError {
    /// When a token doesn't have a valid JWT shape
    #[error("Invalid token")]
    InvalidToken,
    /// When the signature doesn't match
    #[error("Invalid signature")]
    InvalidSignature,
    // Validation errors
    /// When a claim required by the validation is not present
    #[error("Missing required claim: {0}")]
    MissingRequiredClaim(String),
    /// When a token’s `exp` claim indicates that it has expired
    #[error("Expired signature")]
    ExpiredSignature,
    /// When a token’s `iss` claim does not match the expected issuer
    #[error("Invalid issuer")]
    InvalidIssuer,
    /// When a token’s `aud` claim does not match one of the expected audience values
    #[error("Invalid audience")]
    InvalidAudience,
    /// When a token’s `sub` claim does not match one of the expected subject values
    #[error("Invalid subject")]
    InvalidSubject,
    /// When a token’s `nbf` claim represents a time in the future
    #[error("Immature signature")]
    ImmatureSignature,
    /// When the algorithm in the header doesn't match the one passed to `decode` or the encoding/decoding key
    /// used doesn't match the alg requested
    #[error("Invalid algorithm")]
    InvalidAlgorithm,
    // 3rd party errors
    /// An error happened when decoding some base64 text
    #[error("{0}")]
    Base64(#[from] base64::DecodeError),
    /// An error happened while deserializing JSON
    #[error("{0}")]
    Json(#[from] serde_json::Error),
}

const ALGORITHM: &str = "HS256";

fn b64_encode<T: AsRef<[u8]>>(input: T) -> String {
    URL_SAFE_NO_PAD.encode(input)
}

fn b64_decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
    URL_SAFE_NO_PAD.decode(input).map_err(|e| e.into())
}

/// Serializes a struct to JSON and encodes it in base64
fn b64_encode_part<T: Serialize>(input: &T) -> serde_json::Result<String> {
    let json = serde_json::to_vec(input)?;
    Ok(b64_encode(json))
}

pub struct Jwt {
    header: String,
    key: hmac::Key,
}

impl Jwt {
    pub fn new(secret: &[u8]) -> Self {
        let header: String = b64_encode(b"{\"alg\":\"HS256\",\"typ\":\"JWT\"}");
        let key = hmac::Key::new(HMAC_SHA256, secret);
        Self { key, header }
    }

    fn sign(&self, message: &[u8]) -> String {
        let digest = hmac::sign(&self.key, message);
        b64_encode(digest)
    }

    fn verify(&self, signature: &str, message: &[u8]) -> bool {
        let signed = self.sign(message);
        verify_slices_are_equal(signature.as_bytes(), signed.as_bytes()).is_ok()
    }

    pub fn encode<T: Serialize>(&self, claims: &T) -> Result<String, EncodeError> {
        let encoded_claims = b64_encode_part(claims)?;
        let header = &self.header;
        let mut message = String::with_capacity(header.len() + 1 + encoded_claims.len());
        message.push_str(header);
        message.push('.');
        message.push_str(&encoded_claims);

        let signature = self.sign(message.as_bytes());
        message.reserve_exact(signature.len() + 1);
        message.push('.');
        message.push_str(&signature);

        Ok(message)
    }

    pub fn decode<T: DeserializeOwned>(&self, token: &str) -> Result<T, DecodeError> {
        match self.verify_signature(token) {
            Err(e) => Err(e),
            Ok((_, claims)) => {
                let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(claims)?;
                let claims = decoded_claims.deserialize()?;
                self.validate(decoded_claims.deserialize()?)?;
                Ok(claims)
            }
        }
    }

    fn validate(&self, claims: ClaimsForValidation) -> Result<(), DecodeError> {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("Time went backwards")
            .as_secs();
        if !matches!(claims.exp, TryParse::Parsed(_)) {
            return Err(DecodeError::MissingRequiredClaim("exp".to_string()));
        }

        if matches!(claims.exp, TryParse::Parsed(exp) if exp < now - 60) {
            return Err(DecodeError::ExpiredSignature);
        }

        Ok(())
    }

    fn verify_signature<'a>(&self, token: &'a str) -> Result<(Header, &'a str), DecodeError> {
        let (message, signature) = match token.rsplit_once('.') {
            Some(value) => value,
            None => return Err(DecodeError::InvalidToken),
        };

        let (header, payload) = match message.rsplit_once('.') {
            Some(value) => value,
            None => return Err(DecodeError::InvalidToken),
        };

        let header = Header::from_encoded(header)?;

        if header.alg != ALGORITHM {
            return Err(DecodeError::InvalidAlgorithm);
        }

        if !self.verify(signature, message.as_bytes()) {
            return Err(DecodeError::InvalidSignature);
        }

        Ok((header, payload))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
struct Header {
    pub typ: String,
    pub alg: String,
}

impl Header {
    /// Converts an encoded part into the Header struct if possible
    fn from_encoded<T: AsRef<[u8]>>(encoded_part: T) -> Result<Self, DecodeError> {
        let decoded = b64_decode(encoded_part)?;
        Ok(serde_json::from_slice(&decoded)?)
    }
}

impl Default for Header {
    fn default() -> Self {
        Self {
            typ: "JWT".to_string(),
            alg: ALGORITHM.to_string(),
        }
    }
}

/// This is used to decode from base64 then deserialize from JSON to several structs:
/// - The user-provided struct
/// - The ClaimsForValidation struct from this crate to run validation on
struct DecodedJwtPartClaims {
    b64_decoded: Vec<u8>,
}

impl DecodedJwtPartClaims {
    fn from_jwt_part_claims(
        encoded_jwt_part_claims: impl AsRef<[u8]>,
    ) -> Result<Self, DecodeError> {
        Ok(Self {
            b64_decoded: b64_decode(encoded_jwt_part_claims)?,
        })
    }

    fn deserialize<'a, T: Deserialize<'a>>(&'a self) -> Result<T, DecodeError> {
        Ok(serde_json::from_slice(&self.b64_decoded)?)
    }
}

#[derive(Deserialize)]
struct ClaimsForValidation {
    #[serde(deserialize_with = "numeric_type", default)]
    exp: TryParse<u64>,
}

#[derive(Debug)]
enum TryParse<T> {
    Parsed(T),
    FailedToParse,
    NotPresent,
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for TryParse<T> {
    fn deserialize<D: serde::Deserializer<'de>>(
        deserializer: D,
    ) -> std::result::Result<Self, D::Error> {
        Ok(match Option::<T>::deserialize(deserializer) {
            Ok(Some(value)) => TryParse::Parsed(value),
            Ok(None) => TryParse::NotPresent,
            Err(_) => TryParse::FailedToParse,
        })
    }
}

impl<T> Default for TryParse<T> {
    fn default() -> Self {
        Self::NotPresent
    }
}

/// Usually #[serde(borrow)] on `Cow` enables deserializing with no allocations where
/// possible (no escapes in the original str) but it does not work on e.g. `HashSet<Cow<str>>`
/// We use this struct in this case.
#[derive(Deserialize, PartialEq, Eq, Hash)]
struct BorrowedCowIfPossible<'a>(#[serde(borrow)] Cow<'a, str>);
impl std::borrow::Borrow<str> for BorrowedCowIfPossible<'_> {
    fn borrow(&self) -> &str {
        &self.0
    }
}

fn numeric_type<'de, D>(deserializer: D) -> std::result::Result<TryParse<u64>, D::Error>
where
    D: Deserializer<'de>,
{
    struct NumericType(PhantomData<fn() -> TryParse<u64>>);

    impl<'de> Visitor<'de> for NumericType {
        type Value = TryParse<u64>;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("A NumericType that can be reasonably coerced into a u64")
        }

        fn visit_f64<E>(self, value: f64) -> std::result::Result<Self::Value, E>
        where
            E: de::Error,
        {
            if value.is_finite() && value >= 0.0 && value < (u64::MAX as f64) {
                Ok(TryParse::Parsed(value.round() as u64))
            } else {
                Err(serde::de::Error::custom(
                    "NumericType must be representable as a u64",
                ))
            }
        }

        fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
        where
            E: de::Error,
        {
            Ok(TryParse::Parsed(value))
        }
    }

    match deserializer.deserialize_any(NumericType(PhantomData)) {
        Ok(ok) => Ok(ok),
        Err(_) => Ok(TryParse::FailedToParse),
    }
}