Functional Weave
Code in Rust

auth.jwt@1.0.0

impl/rust/verify_jwt.rs

4,554 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::auth_jwt_decode_jwt::{check_jwt_secret, split_jwt};  ← decodeJwt, another function of this group · built into the same file, even by a slim install
use super::crypto_constant_time_equal::constant_time_equal;  ← from crypto.constant-time-equal ^1.0.0 · built alongside by fune
use super::crypto_hmac_sha256::hmac_sha256;  ← from crypto.hmac-sha256 ^1.0.0 · built alongside by fune

fn fail(error: &str, message: &str) -> JwtVerification {
    JwtVerification {
        valid: false,
        claims: None,
        error: Some(error.to_string()),
        message: Some(message.to_string()),
    }
}

/// A present claim, as distinct from an absent one: `Value::get` answers
/// Null for both.
fn field<'v>(object: &'v Value, key: &str) -> Option<&'v Value> {
    match object {
        Value::Obj(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
        _ => None,
    }
}

/// Check an HS256 token: shape, algorithm (so "none" and algorithm-confusion
/// tokens are refused before any key is used), signature in constant time,
/// then exp, nbf and iat against `now` with leeway. A bad token is an
/// answer, never a panic; only a bad secret or leeway panics.
///
/// # Panics
/// Panics if the secret is not at least 32 bytes or the leeway is negative.
pub fn verify_jwt(token: &str, secret: &[i64], now: i64, leeway_seconds: i64) -> JwtVerification {
    check_jwt_secret(secret);
    if leeway_seconds < 0 {
        panic!("leewaySeconds must be a whole number, 0 or more");
    }
    let (header, claims, signing_input, signature) = match split_jwt(token) {
        Some(parts) => parts,
        None => return fail("malformed_token", "the token is not a well-formed JWT"),
    };
    if field(&header, "alg") != Some(&Value::str("HS256")) {
        return fail("unsupported_algorithm", "only HS256 tokens are accepted");
    }
    if field(&header, "crit").is_some() {
        return fail(
            "unsupported_algorithm",
            "the token requires header extensions (crit) this verifier does not support",
        );
    }
    let input: Vec<i64> = signing_input.bytes().map(|b| b as i64).collect();
    if !constant_time_equal(&hmac_sha256(secret, &input), &signature) {
        return fail("invalid_signature", "the token's signature does not match");
    }

    let mut times: [Option<f64>; 3] = [None, None, None];
    for (slot, name) in ["exp", "nbf", "iat"].iter().enumerate() {
        match field(&claims, name) {
            None => {}
            Some(Value::Int(i)) => times[slot] = Some(*i as f64),
            Some(Value::Float(f)) => times[slot] = Some(*f),
            Some(_) => {
                return fail("invalid_claims", &format!("the token's {} claim is not a number", name));
            }
        }
    }
    let now = now as f64;
    let leeway = leeway_seconds as f64;
    // RFC 7519 4.1.4: the current time MUST be before exp.
    if let Some(exp) = times[0] {
        if now >= exp + leeway {
            return fail("token_expired", "the token has expired");
        }
    }
    // RFC 7519 4.1.5: the current time MUST be at or after nbf.
    if let Some(nbf) = times[1] {
        if now + leeway < nbf {
            return fail("token_not_yet_valid", "the token is not valid yet");
        }
    }
    if let Some(iat) = times[2] {
        if iat > now + leeway {
            return fail("token_issued_in_future", "the token was issued in the future");
        }
    }
    JwtVerification { valid: true, claims: Some(claims), error: None, message: None }
}

pub fn jwt_verification_to_value(result: &JwtVerification) -> Value {
    let text = |v: &Option<String>| match v {
        Some(s) => Value::str(s),
        None => Value::Null,
    };
    Value::obj(vec![
        ("valid", Value::Bool(result.valid)),
        ("claims", result.claims.clone().unwrap_or(Value::Null)),
        ("error", text(&result.error)),
        ("message", text(&result.message)),
    ])
}

fn whole(value: &Value, message: &str) -> i64 {
    match value {
        Value::Int(i) => *i,
        _ => panic!("{}", message),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let secret: Vec<i64> = match &args[1] {
        Value::Arr(items) => items
            .iter()
            .map(|item| match item {
                Value::Int(i) => *i,
                _ => panic!("secret must be a list of integers from 0 to 255"),
            })
            .collect(),
        _ => panic!("secret must be a list of integers from 0 to 255"),
    };
    let now = whole(&args[2], "now must be a whole number of Unix seconds");
    let leeway = whole(&args[3], "leewaySeconds must be a whole number, 0 or more");
    jwt_verification_to_value(&verify_jwt(args[0].as_str(), &secret, now, leeway))
}