diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6d691ce6..5487a948 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,6 +18,8 @@ jobs: - uses: actions/checkout@v3 - name: Add musl target run: rustup target add x86_64-unknown-linux-musl + - name: Install musl-gcc + run: sudo apt update && sudo apt install -y musl-tools - name: Format Check run: cargo fmt --check - name: Build diff --git a/Cargo.toml b/Cargo.toml index 894e5759..6d5f3240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ crypto = ["dep:constant_time_eq", "dep:hmac", "dep:hex", "dep:md-5", "dep:sha1", deprecated = [] hex = ["dep:data-encoding"] http = [] -jwt = [] +jwt = ["dep:jsonwebtoken", "dep:data-encoding"] glob = ["dep:wax"] graph = [] jsonschema = ["dep:jsonschema"] @@ -91,7 +91,8 @@ jsonschema = { version = "0.17.1", default-features = false, optional = true } chrono = { version = "0.4.31", optional = true } chrono-tz = { version = "0.8.5", optional = true } compact-rc = "0.5.2" - +jsonwebtoken = { version = "9.2.0", optional = true } +itertools = "0.12.1" [dev-dependencies] clap = { version = "4.4.7", features = ["derive"] } diff --git a/README.md b/README.md index 6e59529e..326321ba 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,6 @@ The following test suites don't pass fully due to mising builtins: - `graphql` - `invalidkeyerror` - `jsonpatch` -- `jwtbuiltins` - `jwtdecodeverify` - `jwtencodesign` - `jwtencodesignraw` diff --git a/src/builtins/jwt.rs b/src/builtins/jwt.rs index 30d4f7cc..a37443db 100644 --- a/src/builtins/jwt.rs +++ b/src/builtins/jwt.rs @@ -3,19 +3,64 @@ use crate::ast::{Expr, Ref}; use crate::builtins; -use crate::builtins::utils::ensure_args_count; +use crate::builtins::utils::{ensure_args_count, ensure_string}; use crate::lexer::Span; use crate::value::Value; +use itertools::Itertools; use std::collections::HashMap; -use anyhow::Result; +use anyhow::{bail, Result}; pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { + m.insert("io.jwt.decode", (jwt_decode, 1)); m.insert("io.jwt.decode_verify", (jwt_decode_verify, 2)); } +fn decode(span: &Span, jwt: String, strict: bool) -> Result { + let Some((Ok(header), Ok(payload), Ok(signature))) = jwt + .split('.') + .map(|p| data_encoding::BASE64URL_NOPAD.decode(p.as_bytes())) + .collect_tuple() + else { + if strict { + bail!(span.error("invalid jwt token")); + } + return Ok(Value::Undefined); + }; + + let header = String::from_utf8_lossy(&header).to_string(); + let payload = String::from_utf8_lossy(&payload).to_string(); + let signature = data_encoding::HEXLOWER_PERMISSIVE.encode(&signature); + + let signature = Value::String(signature.into()); + let header = Value::from_json_str(&header)?; + + if header["enc"] != Value::Undefined { + bail!(span.error("JWT is a JWE object, which is not supported")); + } + + if header["cty"] == "JWT".into() { + if payload.len() <= 2 || !payload.starts_with('"') || !payload.ends_with('"') { + bail!(span.error("invalid nested JWT")); + } + // Ignore "" + decode(span, payload[1..payload.len() - 1].to_string(), strict) + } else { + let payload = Value::from_json_str(&payload)?; + Ok(Value::from_array([header, payload, signature].into())) + } +} + +fn jwt_decode(span: &Span, params: &[Ref], args: &[Value], strict: bool) -> Result { + let name = "io.jwt.decode"; + ensure_args_count(span, name, params, args, 1)?; + let jwt = ensure_string(name, ¶ms[0], &args[0])?; + + decode(span, jwt.to_string(), strict) //header, payload, signature, strict) +} + fn jwt_decode_verify( span: &Span, params: &[Ref], diff --git a/tests/opa.passing b/tests/opa.passing index 013e54d7..240dfbaf 100644 --- a/tests/opa.passing +++ b/tests/opa.passing @@ -54,8 +54,7 @@ jsonfilteridempotent jsonremove jsonremoveidempotent jsonschema -jwtencodesignheadererrors -jwtencodesignpayloaderrors +jwtbuiltins negation nestedreferences numbersrange