diff --git a/plugins/jwt_decoder.lua b/plugins/jwt_decoder.lua new file mode 100644 index 0000000..ce67c57 --- /dev/null +++ b/plugins/jwt_decoder.lua @@ -0,0 +1,80 @@ +Plugin = { + name = "JWT Decoder", + description = [[ +Decodes JWTs found in request headers and exposes the decoded payload inline. + +For every request header whose value looks like a JWT (three dot-separated +base64url segments), the base64url-encoded payload is decoded and added back to +the request as a new header: + +``` +Authorization: Bearer eyJ... -> X-JWT-Decoded-Authorization: {"sub":"123",...} +X-Auth-Token: eyJ... -> X-JWT-Decoded-X-Auth-Token: {"sub":"123",...} +``` + +A leading `Bearer ` prefix is stripped from any header value before decoding, +so custom authorization headers (`Authorization-Test`, `X-Auth-Token`, ...) +are handled too. The decoded header is added to the outbound request, so it is visible +in the intercept, history and replay views (and forwarded upstream). + +Pure Lua, no external dependencies. + ]], + on_request = { sync = true }, +} + +-- Reverse lookup table: base64url character -> 6-bit value (built once at load time). +local B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" +local B64_DEC = {} +for i = 1, #B64_ALPHABET do + B64_DEC[B64_ALPHABET:sub(i, i)] = i - 1 +end + +-- Decode an unpadded base64url string. Returns the decoded string, or nil on invalid input. +local function b64url_decode(s) + if #s % 4 == 1 then + return nil + end + local out, acc, bits = {}, 0, 0 + for c in s:gmatch(".") do + local v = B64_DEC[c] + if not v then + return nil + end + acc, bits = acc * 64 + v, bits + 6 + if bits >= 8 then + bits = bits - 8 + out[#out + 1] = string.char(math.floor(acc / 2 ^ bits)) + acc = acc % 2 ^ bits + end + end + return table.concat(out) +end + +-- If value is a JWT, return its decoded payload +local function decode_jwt_payload(value) + -- Three dot-separated base64url segments; the signature may be empty (alg:none). + local payload = value:match("^[A-Za-z0-9_-]+%.([A-Za-z0-9_-]+)%.[A-Za-z0-9_-]*$") + if not payload or #payload > 8192 then + return nil + end + local decoded = b64url_decode(payload) + if not decoded then + return nil + end + -- Only accept plausible JSON payloads. + if decoded:match("^%s*{") == nil then + return nil + end + decoded = decoded:gsub("[%c]+", " ") + return decoded +end + +function on_request(req) + for name, value in pairs(req.headers) do + local token = value:gsub("^[Bb][Ee][Aa][Rr][Ee][Rr]%s+", "") + local payload = decode_jwt_payload(token) + if payload then + req:set_header("X-JWT-Decoded-" .. name, payload) + end + end +end