Functional Weave
Code in Python

auth.access-token@1.0.0

impl/rust/read_access_token.rs

4,462 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_bearer_token::parse_bearer_token;  ← from auth.bearer-token ^1.0.0 · built alongside by fune
use super::auth_jwt_decode_jwt::check_jwt_secret;
use super::auth_jwt_verify_jwt::verify_jwt;
use super::time_unix_to_iso::unix_to_iso;  ← from time.unix-to-iso ^1.0.0 · built alongside by fune

/// The latest second time.unix-to-iso can write.
const MAX_EXP: i64 = 253402300799;

/// A failed check: every field but the reason is `None`.
pub fn access_denied(error: &str, message: &str) -> AccessCheck {
    AccessCheck {
        ok: false,
        subject: None,
        token_version: None,
        jti: None,
        expires_at: None,
        claims: None,
        error: Some(error.to_string()),
        message: Some(message.to_string()),
    }
}

/// A JSON number with no fraction; `1.0` is whole, as it is in JavaScript.
fn whole(value: &Value) -> Option<i64> {
    match value {
        Value::Int(i) => Some(*i),
        Value::Float(f) if f.is_finite() && f.fract() == 0.0 && f.abs() < 9.007_199_254_740_992e15 => Some(*f as i64),
        _ => None,
    }
}

/// Who is making this request? The Bearer token from the Authorization
/// header, verified, and required to carry the claims issue_access_token
/// writes. A missing or bad token is an answer (401), never a panic.
///
/// # Panics
/// Panics on a secret under 32 bytes or a negative leeway.
pub fn read_access_token(authorization: Option<&str>, secret: &[i64], now: i64, leeway_seconds: i64) -> AccessCheck {
    check_jwt_secret(secret);
    if leeway_seconds < 0 {
        panic!("leewaySeconds must be a whole number, 0 or more");
    }
    let token = match parse_bearer_token(authorization) {
        Some(token) => token,
        None => return access_denied("missing_token", "the request has no Bearer token"),
    };
    let verified = verify_jwt(&token, secret, now, leeway_seconds);
    let claims = match (verified.valid, verified.claims) {
        (true, Some(claims)) => claims,
        _ => {
            return access_denied(
                verified.error.as_deref().unwrap_or("malformed_token"),
                verified.message.as_deref().unwrap_or("the token is not a well-formed JWT"),
            )
        }
    };
    let sub = match claims.get("sub") {
        Value::Str(s) if !s.is_empty() => Some(s.clone()),
        _ => None,
    };
    let jti = match claims.get("jti") {
        Value::Str(s) if !s.is_empty() => Some(s.clone()),
        _ => None,
    };
    let ver = whole(claims.get("ver")).filter(|v| *v >= 0);
    let exp = whole(claims.get("exp")).filter(|e| (0..=MAX_EXP).contains(e));
    match (sub, jti, ver, exp) {
        (Some(subject), Some(jti), Some(ver), Some(exp)) => AccessCheck {
            ok: true,
            subject: Some(subject),
            token_version: Some(ver),
            jti: Some(jti),
            expires_at: Some(unix_to_iso(exp)),
            claims: Some(claims),
            error: None,
            message: None,
        },
        _ => access_denied("invalid_claims", "the token lacks the sub, jti, ver or exp this API issues"),
    }
}

pub fn access_check_to_value(check: &AccessCheck) -> Value {
    let text = |v: &Option<String>| v.as_deref().map(Value::str).unwrap_or(Value::Null);
    Value::obj(vec![
        ("ok", Value::Bool(check.ok)),
        ("subject", text(&check.subject)),
        ("tokenVersion", check.token_version.map(Value::Int).unwrap_or(Value::Null)),
        ("jti", text(&check.jti)),
        ("expiresAt", text(&check.expires_at)),
        ("claims", check.claims.clone().unwrap_or(Value::Null)),
        ("error", text(&check.error)),
        ("message", text(&check.message)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let header = match &args[0] {
        Value::Str(s) => Some(s.as_str()),
        _ => None,
    };
    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 = match &args[2] {
        Value::Int(i) => *i,
        _ => panic!("now must be a whole number of Unix seconds"),
    };
    let leeway = match &args[3] {
        Value::Int(i) => *i,
        _ => panic!("leewaySeconds must be a whole number, 0 or more"),
    };
    access_check_to_value(&read_access_token(header, &secret, now, leeway))
}