Functional Weave
Code in Python

auth.jwt@1.0.0

impl/rust/decode_jwt.rs

10,817 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::encoding_base64_base64_url_decode::base64_url_decode;

/// Containers nested deeper than this are refused, in every language, rather
/// than left to each parser's own limit.
const MAX_DEPTH: usize = 32;
const MAX_SAFE_F64: f64 = 9007199254740991.0;

fn sextet(b: u8) -> i64 {
    match b {
        b'A'..=b'Z' => (b - b'A') as i64,
        b'a'..=b'z' => (b - b'a' + 26) as i64,
        b'0'..=b'9' => (b - b'0' + 52) as i64,
        b'-' => 62,
        b'_' => 63,
        _ => -1,
    }
}

/// One canonical, unpadded base64url segment? Checked before decoding so a
/// hostile token is an answer (`None`), never a panic.
fn is_segment(text: &str, allow_empty: bool) -> bool {
    let b = text.as_bytes();
    if b.is_empty() {
        return allow_empty;
    }
    if b.len() % 4 == 1 {
        return false;
    }
    let mut last = 0;
    for &c in b {
        last = sextet(c);
        if last < 0 {
            return false;
        }
    }
    match b.len() % 4 {
        2 => last & 15 == 0,
        3 => last & 3 == 0,
        _ => true,
    }
}

/// A strict RFC 8259 parser. The funejson runtime's parser is built for test
/// vectors and is lenient (a leading +, any Unicode whitespace); a token from
/// the network has to be read exactly as TypeScript's JSON.parse and
/// Python's json.loads read it, with the same refusals: integers beyond
/// 2^53, non-finite numbers, lone surrogates and nesting past 32.
struct Parser<'a> {
    b: &'a [u8],
    pos: usize,
}

impl<'a> Parser<'a> {
    fn ws(&mut self) {
        while self.pos < self.b.len() && matches!(self.b[self.pos], b' ' | b'\t' | b'\n' | b'\r') {
            self.pos += 1;
        }
    }

    fn peek(&self) -> Option<u8> {
        self.b.get(self.pos).copied()
    }

    fn literal(&mut self, word: &[u8], value: Value) -> Option<Value> {
        if self.b.len() >= self.pos + word.len() && &self.b[self.pos..self.pos + word.len()] == word {
            self.pos += word.len();
            Some(value)
        } else {
            None
        }
    }

    fn value(&mut self, depth: usize) -> Option<Value> {
        self.ws();
        match self.peek()? {
            b'{' => {
                if depth > MAX_DEPTH {
                    return None;
                }
                self.pos += 1;
                let mut pairs: Vec<(String, Value)> = Vec::new();
                self.ws();
                if self.peek() == Some(b'}') {
                    self.pos += 1;
                    return Some(Value::Obj(pairs));
                }
                loop {
                    self.ws();
                    if self.peek()? != b'"' {
                        return None;
                    }
                    let key = self.string()?;
                    self.ws();
                    if self.peek()? != b':' {
                        return None;
                    }
                    self.pos += 1;
                    let item = self.value(depth + 1)?;
                    // A repeated key keeps its first position and its last
                    // value, as JavaScript objects and Python dicts do.
                    match pairs.iter_mut().find(|(k, _)| *k == key) {
                        Some(slot) => slot.1 = item,
                        None => pairs.push((key, item)),
                    }
                    self.ws();
                    match self.peek()? {
                        b',' => self.pos += 1,
                        b'}' => {
                            self.pos += 1;
                            return Some(Value::Obj(pairs));
                        }
                        _ => return None,
                    }
                }
            }
            b'[' => {
                if depth > MAX_DEPTH {
                    return None;
                }
                self.pos += 1;
                let mut items = Vec::new();
                self.ws();
                if self.peek() == Some(b']') {
                    self.pos += 1;
                    return Some(Value::Arr(items));
                }
                loop {
                    items.push(self.value(depth + 1)?);
                    self.ws();
                    match self.peek()? {
                        b',' => self.pos += 1,
                        b']' => {
                            self.pos += 1;
                            return Some(Value::Arr(items));
                        }
                        _ => return None,
                    }
                }
            }
            b'"' => self.string().map(Value::Str),
            b't' => self.literal(b"true", Value::Bool(true)),
            b'f' => self.literal(b"false", Value::Bool(false)),
            b'n' => self.literal(b"null", Value::Null),
            _ => self.number(),
        }
    }

    fn hex4(&mut self) -> Option<u32> {
        if self.pos + 4 > self.b.len() {
            return None;
        }
        let mut n = 0u32;
        for &c in &self.b[self.pos..self.pos + 4] {
            n = n * 16 + (c as char).to_digit(16)?;
        }
        self.pos += 4;
        Some(n)
    }

    fn string(&mut self) -> Option<String> {
        self.pos += 1; // opening quote
        let mut out: Vec<u8> = Vec::new();
        loop {
            let c = self.peek()?;
            self.pos += 1;
            match c {
                b'"' => return String::from_utf8(out).ok(),
                b'\\' => {
                    let e = self.peek()?;
                    self.pos += 1;
                    let ch = match e {
                        b'"' => '"',
                        b'\\' => '\\',
                        b'/' => '/',
                        b'b' => '\u{8}',
                        b'f' => '\u{c}',
                        b'n' => '\n',
                        b'r' => '\r',
                        b't' => '\t',
                        b'u' => {
                            let hi = self.hex4()?;
                            let code = if (0xD800..=0xDBFF).contains(&hi) {
                                if self.b.get(self.pos) != Some(&b'\\') || self.b.get(self.pos + 1) != Some(&b'u') {
                                    return None;
                                }
                                self.pos += 2;
                                let lo = self.hex4()?;
                                if !(0xDC00..=0xDFFF).contains(&lo) {
                                    return None;
                                }
                                0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00)
                            } else {
                                hi
                            };
                            // A lone low surrogate has no char and is refused here.
                            char::from_u32(code)?
                        }
                        _ => return None,
                    };
                    let mut buf = [0u8; 4];
                    out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
                }
                0x00..=0x1f => return None,
                other => out.push(other),
            }
        }
    }

    fn digits(&mut self) -> usize {
        let start = self.pos;
        while self.pos < self.b.len() && self.b[self.pos].is_ascii_digit() {
            self.pos += 1;
        }
        self.pos - start
    }

    fn number(&mut self) -> Option<Value> {
        let start = self.pos;
        if self.peek() == Some(b'-') {
            self.pos += 1;
        }
        match self.peek()? {
            b'0' => self.pos += 1,
            b'1'..=b'9' => {
                self.digits();
            }
            _ => return None,
        }
        let mut is_float = false;
        if self.peek() == Some(b'.') {
            self.pos += 1;
            if self.digits() == 0 {
                return None;
            }
            is_float = true;
        }
        if matches!(self.peek(), Some(b'e') | Some(b'E')) {
            self.pos += 1;
            if matches!(self.peek(), Some(b'+') | Some(b'-')) {
                self.pos += 1;
            }
            if self.digits() == 0 {
                return None;
            }
            is_float = true;
        }
        let text = std::str::from_utf8(&self.b[start..self.pos]).ok()?;
        if !is_float {
            if let Ok(i) = text.parse::<i64>() {
                return if (i as f64).abs() <= MAX_SAFE_F64 { Some(Value::Int(i)) } else { None };
            }
        }
        let f: f64 = text.parse().ok()?;
        if !f.is_finite() || (f.fract() == 0.0 && f.abs() > MAX_SAFE_F64) {
            return None;
        }
        Some(Value::Float(f))
    }
}

fn parse_object(segment: &str) -> Option<Value> {
    let bytes: Vec<u8> = base64_url_decode(segment).into_iter().map(|b| b as u8).collect();
    let text = String::from_utf8(bytes).ok()?;
    let mut parser = Parser { b: text.as_bytes(), pos: 0 };
    parser.ws();
    if parser.peek() != Some(b'{') {
        return None;
    }
    let value = parser.value(1)?;
    parser.ws();
    if parser.pos != parser.b.len() {
        return None;
    }
    Some(value)
}

/// (header, claims, signing input, signature bytes) of a compact JWT, or
/// `None` when it is not three canonical base64url segments whose first two
/// are JSON objects. Nothing is verified here; `verify_jwt` builds on it.
pub fn split_jwt(token: &str) -> Option<(Value, Value, String, Vec<i64>)> {
    let parts: Vec<&str> = token.split('.').collect();
    if parts.len() != 3 {
        return None;
    }
    if !is_segment(parts[0], false) || !is_segment(parts[1], false) || !is_segment(parts[2], true) {
        return None;
    }
    let header = parse_object(parts[0])?;
    let claims = parse_object(parts[1])?;
    Some((header, claims, format!("{}.{}", parts[0], parts[1]), base64_url_decode(parts[2])))
}

/// RFC 7518 section 3.2: an HS256 key MUST be at least 256 bits.
///
/// # Panics
/// Panics on a value outside 0-255 or fewer than 32 bytes.
pub fn check_jwt_secret(secret: &[i64]) {
    if secret.iter().any(|b| !(0..=255).contains(b)) {
        panic!("secret must be a list of integers from 0 to 255");
    }
    if secret.len() < 32 {
        panic!(
            "secret must be at least 32 bytes (RFC 7518 section 3.2), received {}",
            secret.len()
        );
    }
}

/// The claims of a JWT WITHOUT checking its signature, or `None` when it is
/// malformed. For reading exp or name from a token you hold; never for
/// deciding whether to trust one.
pub fn decode_jwt(token: &str) -> Option<Value> {
    split_jwt(token).map(|(_, claims, _, _)| claims)
}

pub fn fune_vector(args: &[Value]) -> Value {
    match &args[0] {
        Value::Str(token) => decode_jwt(token).unwrap_or(Value::Null),
        _ => Value::Null,
    }
}