Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions plugins/jwt_decoder.lua
Original file line number Diff line number Diff line change
@@ -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