HAProxy ACL With Variable In Substring

I ended up building the following setup to solve this issue.

data_extractor.lua

-- Function to traverse the table and get a value based on the key.
-- @param input_tbl table: Table to traverse.
-- @param input_field_key string: Input string key to search for.
-- @param input_field_value string: Input string key value to search for.
-- @param input_key_ignore_case boolean: Match the key, regardless of the case of the strings.
-- @param input_value_ignore_case boolean: Match the value, regardless of the case of the strings.
-- @return True or False if key and value can be found
local function traverse_table_keys(input_tbl, input_field_key, input_field_value, input_key_ignore_case, input_value_ignore_case)

    -- Loop through key value pairs in table
    for key, value in pairs(input_tbl) do

        if (input_key_ignore_case) then
            key = string.lower(key)
            input_field_key = string.lower(input_field_key)
        end

        -- If the key and the input field key matches then proceed with checking the type of the value
        if (key ~= nil and key == input_field_key) then

            -- If the value is not of type table and the value matches the input field value, then return true
            if (type(value) ~= "table") then
                if (input_value_ignore_case) then
                    value = string.lower(value)
                    input_field_value = string.lower(input_field_value)
                end

                if (value == input_field_value) then
                    return true
                end
            -- If the value is of type table, the proceed with decoding the table
            else

                -- Loop through key value pairs in table
                for keyTbl, valueTbl in pairs(value) do

                    if (input_value_ignore_case) then
                        valueTbl = string.lower(valueTbl)
                        input_field_value = string.lower(input_field_value)
                    end

                    -- If the value in the table matches the input field value, then return true
                    if (valueTbl ~= nil and valueTbl == input_field_value) then
                        return true
                    end
                end
            end
        -- If the value is of type table, run the table through this function again
        elseif type(value) == "table" then
            if (traverse_table_keys(value, input_field_key, input_field_value, input_key_ignore_case, input_value_ignore_case)) then
                return true
            end
        end
    end

    -- If nothing found then return false
    return false
end

-- Function to parse input object, retrieve specific key and value and compare it to the validation field
-- @param txn HaProxyObject: Object passed from HaProxy to get fetch samples.
-- @param input_object string: The variable containing the JSON packet that should be traveresed.
-- @param input_key string: The variable containing the key name that should be retrieved.
-- @param input_validation string: The variable containing the validation value that should exist within the value for the input_key.
-- @param input_key_ignore_case boolean: Match the key, regardless of the case of the strings.
-- @param input_value_ignore_case boolean: Match the value, regardless of the case of the strings.
-- @param input_debug boolean: If additional debug logging should be printed or not.
-- @return string: Boolean to confirm whether input_validation could be found within the value of the input_key field within the input_object JSON object.
local function json_extract_and_compare(txn, input_object, input_key, input_validation, input_key_ignore_case, input_value_ignore_case, input_debug)
    local fe_name = txn.sf:fe_name()
    local luaId = (txn.get_var(txn, "txn.luaId") or "N/A")
    local inputObject = (txn:get_var(input_object) or "N/A")
    local inputKey = (txn:get_var(input_key) or "N/A")
    local inputValidation = (txn:get_var(input_validation) or "N/A")

    -- Validate inputs
    -- Argument 1: input_object / inputObject
    if (inputObject == nil or type(inputObject) ~= "string" or string.len(inputObject) == 0) then
        print("Argument 1 (input_object) is mandatory and has to be of type string and has to be a well formed JSON packet [" .. fe_name .. "]")
        return ""
    end

    -- Argument 2: input_key / inputKey
    if (inputKey == nil or type(inputKey) ~= "string") then
        print("Argument 2 (input_key) is mandatory and has to be of type string [" .. fe_name .. "]")
        return ""
    end

    -- Argument 3: input_validation / inputValidation
    if (inputValidation == nil or type(inputValidation) ~= "string") then
        print("Argument 3 (input_validation) is mandatory and has to be of type string [" .. fe_name .. "]")
        return ""
    end

    -- Argument 4: input_key_ignore_case
    if (input_key_ignore_case ~= nil) then
        -- Convert from string to boolean representation
        if (input_key_ignore_case == "true") then
            input_key_ignore_case = true
        elseif (input_key_ignore_case == "false") then
            input_key_ignore_case = false
        else
            print("Argument 4 (input_key_ignore_case) has to be of type boolean [" .. fe_name .. "]")
            return ""
        end
    else
        -- Default to false
        input_key_ignore_case = false
    end

    -- Argument 5: input_value_ignore_case
    if (input_value_ignore_case ~= nil) then
        -- Convert from string to boolean representation
        if (input_value_ignore_case == "true") then
            input_value_ignore_case = true
        elseif (input_value_ignore_case == "false") then
            input_value_ignore_case = false
        else
            print("Argument 5 (input_value_ignore_case) has to be of type boolean [" .. fe_name .. "]")
            return ""
        end
    else
        -- Default to false
        input_value_ignore_case = false
    end

    -- Argument 6: input_debug
    if (input_debug ~= nil) then
        -- Convert from string to boolean representation
        if (input_debug == "true") then
            input_debug = true
        elseif (input_debug == "false") then
            input_debug = false
        else
            print("Argument 6 (input_debug) has to be of type boolean [" .. fe_name .. "]")
            return ""
        end
    else
        -- Default to false
        input_debug = false
    end

    -- Print debug if required
    if (input_debug) then
        print("Frontend: " .. fe_name)
        print("Input Object: " .. inputObject)
        print("Input Key: " .. inputKey)
        print("Input Validation: " .. inputValidation)
        print("Input Key Ignore Case: " .. tostring(input_key_ignore_case))
        print("Input Value Ignore Case: " .. tostring(input_value_ignore_case))
    end

    -- Decode as JSON and traverse table key and values
    local extract_and_compare_output = traverse_table_keys(json.decode(inputObject), inputKey, inputValidation, input_key_ignore_case,
    input_value_ignore_case)

    -- Print debug if required
    if (input_debug) then
        print("Extract And Compare Output: " .. tostring(extract_and_compare_output))
    end

    return extract_and_compare_output
end

core.register_fetches("json_extract_and_compare", json_extract_and_compare)

haproxy.cfg:

global
  lua-load-per-thread /opt/haproxy/lua/data_extractor.lua

frontend test-8080
  bind *:8080
  
  http-request set-var(txn.jwt_payload) req.hdr(Authorization),word(2,.),ub64dec

  acl allowed_origins_in_payload var(txn.jwt_payload) -m found -m sub '"allowed-origins":'

  http-request set-var(txn.jwt_payload_key) str("allowed-origins") if allowed_origins_in_payload

  http-request set-var(txn.jwt_source_validation) src if allowed_origins_in_payload
  
  acl wildcard_in_allowed_origins http_auth_bearer,jwt_payload_query('$.allowed-origins[0]') -m str '*' '/*'

  http-request set-var(txn.allowed_origins_passed) lua.json_extract_and_compare(txn.jwt_payload,txn.jwt_payload_key,txn.jwt_source_validation,false) if allowed_origins_in_payload !wildcard_in_allowed_origins

  http-request deny deny_status 403 content-type 'application/json' string '{ "error": "Access Forbidden"}' if allowed_origins_in_payload !{ var(txn.allowed_origins_passed) -m bool } !wildcard_in_allowed_origins