jwt-lab 0.1.1

JWT crate for Rust: decode, verify, sign, mutate, JWK/JWKS, algorithm selection, time validation, and secure APIs.
Documentation
use crate::{types::{Jwt, Algorithm}, jwk::{Key, Jwks}, time::{Leeway, validate_claim_times}, Error, Result};

/// Options for JWT verification
#[derive(Debug, Clone)]
pub struct VerifyOptions {
    /// Whether to validate time-based claims (exp, nbf)
    pub validate_times: bool,
    /// Leeway in seconds for time-based claim validation
    pub leeway: Leeway,
    /// Expected issuer claim value
    pub expected_iss: Option<String>,
    /// Expected audience claim value
    pub expected_aud: Option<String>,
    /// Expected algorithm (prevents algorithm confusion attacks)
    pub expected_alg: Option<Algorithm>,
    /// Current timestamp for time validation (defaults to current time)
    pub now_ts: Option<i64>,
}

impl Default for VerifyOptions {
    fn default() -> Self {
        Self {
            validate_times: true,
            leeway: Leeway { seconds: 0 },
            expected_iss: None,
            expected_aud: None,
            expected_alg: None,
            now_ts: None,
        }
    }
}
impl VerifyOptions {
    /// Set whether to validate time-based claims
    pub fn validate_times(mut self, v: bool) -> Self { self.validate_times = v; self }
    /// Set the leeway in seconds for time validation
    pub fn leeway(mut self, s: i64) -> Self { self.leeway = Leeway { seconds: s }; self }
    /// Set the expected issuer claim value
    pub fn expect_iss(mut self, iss: impl Into<String>) -> Self { self.expected_iss = Some(iss.into()); self }
    /// Set the expected audience claim value
    pub fn expect_aud(mut self, aud: impl Into<String>) -> Self { self.expected_aud = Some(aud.into()); self }
    /// Set the expected algorithm (prevents algorithm confusion attacks)
    pub fn expect_alg(mut self, alg: Algorithm) -> Self { self.expected_alg = Some(alg); self }
    /// Set the current timestamp for time validation
    pub fn now_ts(mut self, ts: i64) -> Self { self.now_ts = Some(ts); self }
}

/// Convert our Algorithm enum to jsonwebtoken's Algorithm enum
fn to_jalg(a: Algorithm) -> jsonwebtoken::Algorithm {
    use Algorithm::*;
    match a {
        HS256 => jsonwebtoken::Algorithm::HS256,
        HS384 => jsonwebtoken::Algorithm::HS384,
        HS512 => jsonwebtoken::Algorithm::HS512,
        RS256 => jsonwebtoken::Algorithm::RS256,
        RS384 => jsonwebtoken::Algorithm::RS384,
        RS512 => jsonwebtoken::Algorithm::RS512,
        ES256 => jsonwebtoken::Algorithm::ES256,
        ES384 => jsonwebtoken::Algorithm::ES384,
        ES512 => jsonwebtoken::Algorithm::ES384,
        EdDSA => jsonwebtoken::Algorithm::EdDSA,
    }
}

/// Verify a JWT with the given key and options
///
/// # Arguments
///
/// * `jwt` - The JWT to verify
/// * `key` - The verification key
/// * `opts` - Verification options
///
/// # Returns
///
/// Returns `Ok(())` if verification succeeds, or an error if it fails
///
/// # Errors
///
/// - `Error::Claims` if algorithm mismatch or claim validation fails
/// - `Error::DisabledAlg` if the algorithm is not enabled via feature flags
/// - `Error::Key` if the key is invalid
/// - `Error::Signature` if signature verification fails
pub fn verify_with_key(jwt: &Jwt, key: &Key, opts: &VerifyOptions) -> Result<()> {
    use jsonwebtoken::{Validation, DecodingKey, decode};

    if let Some(expected_alg) = opts.expected_alg {
        if jwt.header.alg != expected_alg {
            return Err(Error::Claims(format!("algorithm mismatch: expected {:?}, got {:?}", expected_alg, jwt.header.alg)));
        }
    }

    match jwt.header.alg {
        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { #[cfg(not(feature="hs"))] return Err(Error::DisabledAlg("HS")); }
        Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => { #[cfg(not(feature="rs"))] return Err(Error::DisabledAlg("RS")); }
        Algorithm::ES256 | Algorithm::ES384 | Algorithm::ES512 => { #[cfg(not(feature="es"))] return Err(Error::DisabledAlg("ES")); }
        Algorithm::EdDSA => { #[cfg(not(feature="eddsa"))] return Err(Error::DisabledAlg("EdDSA")); }
    }

    let mut v = Validation::new(to_jalg(jwt.header.alg));
    v.validate_exp = false;
    v.insecure_disable_signature_validation();
    if let Some(ref iss) = opts.expected_iss { v.set_issuer(&[iss.clone()]); }
    if let Some(ref aud) = opts.expected_aud { v.set_audience(&[aud.clone()]); }

    let dkey = match key {
        Key::Hs(s) => DecodingKey::from_secret(s),
        Key::RsaPublicPem(p) => DecodingKey::from_rsa_pem(p).map_err(|e| Error::Key(e.to_string()))?,
        Key::EcPublicPem(p) => DecodingKey::from_ec_pem(p).map_err(|e| Error::Key(e.to_string()))?,
        Key::EdPublicPem(p) => DecodingKey::from_ed_pem(p).map_err(|e| Error::Key(e.to_string()))?,
        _ => return Err(Error::Key("public or shared secret required to verify".into())),
    };

    let token_str = format!("{}.{}.{}", jwt.raw_header_b64, jwt.raw_payload_b64, jwt.signature_b64);
    let data = decode::<serde_json::Value>(&token_str, &dkey, &v).map_err(|e| Error::Signature(e.to_string()))?;

    if opts.validate_times {
        let now = opts.now_ts.unwrap_or_else(|| time::OffsetDateTime::now_utc().unix_timestamp());
        let map = data.claims.as_object().cloned().unwrap_or_default();
        validate_claim_times(&map, true, opts.leeway, now)?;
    }
    Ok(())
}

impl Jwt {
    /// Verify this JWT with the given key and options
    ///
    /// # Arguments
    ///
    /// * `key` - The verification key
    /// * `opts` - Verification options
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` if verification succeeds, or an error if it fails
    pub fn verify(&self, key: &Key, opts: VerifyOptions) -> Result<()> {
        verify_with_key(self, key, &opts)
    }
    /// Verify this JWT using a JWKS (JSON Web Key Set)
    ///
    /// # Arguments
    ///
    /// * `jwks` - The JWKS containing the verification keys
    /// * `opts` - Verification options
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` if verification succeeds, or an error if it fails
    ///
    /// # Errors
    ///
    /// - `Error::MissingKid` if the JWT header doesn't contain a `kid` field
    /// - `Error::KidNotFound` if the `kid` is not found in the JWKS
    pub fn verify_with_jwks(&self, jwks: &Jwks, opts: VerifyOptions) -> Result<()> {
        let kid = self.header.kid.clone().ok_or(crate::Error::MissingKid)?;
        let key = jwks.select_for(&kid, self.header.alg)?;
        verify_with_key(self, &key, &opts)
    }
}