Functional Weave
Code in TypeScript

auth.jwt@1.0.0

impl/rust/sign_jwt.rs

4,201 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;  ← decodeJwt, another function of this group · built into the same file, even by a slim install
use super::crypto_hmac_sha256::hmac_sha256;  ← from crypto.hmac-sha256 ^1.0.0 · built alongside by fune
use super::encoding_base64_base64_url_encode::base64_url_encode;

/// base64url of {"alg":"HS256","typ":"JWT"}: the header is fixed.
const HEADER: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
const MAX_DEPTH: usize = 32;
const MAX_SAFE: i64 = 9007199254740991;

fn quote(text: &str, out: &mut String) {
    out.push('"');
    for ch in text.chars() {
        match ch {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            '\u{8}' => out.push_str("\\b"),
            '\u{c}' => out.push_str("\\f"),
            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
            c => out.push(c),
        }
    }
    out.push('"');
}

/// One canonical JSON text: no spaces, object keys sorted by code point
/// (Rust's `String` order), whole numbers only, non-ASCII written as UTF-8.
fn canonical(value: &Value, depth: usize, out: &mut String) {
    const WHOLE: &str = "claims may only hold whole numbers from -(2^53 - 1) to 2^53 - 1";
    match value {
        Value::Null => out.push_str("null"),
        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
        Value::Int(i) => {
            if i.abs() > MAX_SAFE {
                panic!("{}", WHOLE);
            }
            out.push_str(&i.to_string());
        }
        Value::Float(f) => {
            if !f.is_finite() || f.fract() != 0.0 || f.abs() > MAX_SAFE as f64 {
                panic!("{}", WHOLE);
            }
            out.push_str(&(*f as i64).to_string());
        }
        Value::Str(s) => quote(s, out),
        Value::Arr(items) => {
            if depth > MAX_DEPTH {
                panic!("claims are nested more than 32 deep");
            }
            out.push('[');
            for (i, item) in items.iter().enumerate() {
                if i > 0 {
                    out.push(',');
                }
                canonical(item, depth + 1, out);
            }
            out.push(']');
        }
        Value::Obj(pairs) => {
            if depth > MAX_DEPTH {
                panic!("claims are nested more than 32 deep");
            }
            let mut sorted: Vec<&(String, Value)> = pairs.iter().collect();
            sorted.sort_by(|a, b| a.0.cmp(&b.0));
            out.push('{');
            for (i, (key, item)) in sorted.into_iter().enumerate() {
                if i > 0 {
                    out.push(',');
                }
                quote(key, out);
                out.push(':');
                canonical(item, depth + 1, out);
            }
            out.push('}');
        }
    }
}

/// An HS256 JWT for the claims: header {"alg":"HS256","typ":"JWT"}, the
/// claims as canonical JSON, and the HMAC-SHA256 of the two under the secret.
///
/// # Panics
/// Panics if the secret is under 32 bytes, the claims are not an object, or
/// they hold a fraction, an unsafe integer or nesting past 32.
pub fn sign_jwt(claims: &Value, secret: &[i64]) -> String {
    check_jwt_secret(secret);
    if !matches!(claims, Value::Obj(_)) {
        panic!("claims must be a JSON object");
    }
    let mut payload = String::new();
    canonical(claims, 1, &mut payload);
    let payload_bytes: Vec<i64> = payload.bytes().map(|b| b as i64).collect();
    let signing_input = format!("{}.{}", HEADER, base64_url_encode(&payload_bytes));
    let input: Vec<i64> = signing_input.bytes().map(|b| b as i64).collect();
    format!("{}.{}", signing_input, base64_url_encode(&hmac_sha256(secret, &input)))
}

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"),
    };
    Value::str(&sign_jwt(&args[0], &secret))
}