Skip to content

Instantly share code, notes, and snippets.

@inaiat
Last active February 5, 2024 06:29
Show Gist options
  • Save inaiat/02bf5d11732d8e4d7b7546399a3a49af to your computer and use it in GitHub Desktop.
Save inaiat/02bf5d11732d8e4d7b7546399a3a49af to your computer and use it in GitHub Desktop.
Lua script to decode jwt token
-- Json Parsing based on https://gist.github.com/tylerneylon/59f4bcf316be525b30ab
-- Base64 decoding based on wikipedia description of 8/6bit encoding.
-- base64 char array.. note final 2 chars are for RFC4648-URL encoding
-- as per JWT spec section 2 terminology 'Base64url Encoding'
local alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
-- convert to 6 char long binary string. (max int 64!)
local function toBinaryString(int)
if int > 64 then error("Bad number " .. int .. " to convert to binary") end
local remaining = tonumber(int)
local bits = ''
for i = 5, 0, -1 do
local pow = 2 ^ i
if remaining >= pow then
bits = bits .. '1'
remaining = remaining - pow
else
bits = bits .. '0'
end
end
return bits
end
local function fromBinaryString(bits) return tonumber(bits, 2) end
local function decodeBase64(encoded)
local bitstr = ''
local decoded = ''
-- decode chars into bitstring
for i = 1, string.len(encoded) do
local offset, _ = string.find(alpha, string.sub(encoded, i, i))
if offset == nil then
error("Bad base64 character " .. string.sub(encoded, i, i))
end
bitstr = bitstr .. toBinaryString(offset - 1)
end
-- decode bitstring back to chars
for i = 1, string.len(bitstr), 8 do
decoded = decoded ..
string.char(fromBinaryString(string.sub(bitstr, i, i + 7)))
end
return decoded
end
-- json handling
local json = {}
-- Returns pos, did_find; there are two cases:
-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true.
-- 2. Delimiter not found: pos = pos after leading space; did_find = false.
-- This throws an error if err_if_missing is true and the delim is not found.
local function skip_delim(str, pos, delim, err_if_missing)
pos = pos + #str:match('^%s*', pos)
if str:sub(pos, pos) ~= delim then
if err_if_missing then
error('Expected ' .. delim .. ' near position ' .. pos)
end
return pos, false
end
return pos + 1, true
end
-- Expects the given pos to be the first character after the opening quote.
-- Returns val, pos; the returned pos is after the closing quote character.
local function parse_str_val(str, pos, val)
val = val or ''
local early_end_error = 'End of input found while parsing string.'
if pos > #str then error(early_end_error) end
local c = str:sub(pos, pos)
if c == '"' then return val, pos + 1 end
if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end
-- We must have a \ character.
local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'}
local nextc = str:sub(pos + 1, pos + 1)
if not nextc then error(early_end_error) end
return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc))
end
-- Returns val, pos; the returned pos is after the number's final character.
local function parse_num_val(str, pos)
local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
local val = tonumber(num_str)
if not val then error('Error parsing number at position ' .. pos .. '.') end
return val, pos + #num_str
end
json.null = {} -- one-off table to represent the null value.
function json.parse(str, pos, end_delim)
pos = pos or 1
if pos > #str then error('Reached unexpected end of input.') end
local pos = pos + #str:match('^%s*', pos) -- Skip whitespace.
local first = str:sub(pos, pos)
if first == '{' then -- Parse an object.
local obj, key, delim_found = {}, true, true
pos = pos + 1
while true do
key, pos = json.parse(str, pos, '}')
if key == nil then return obj, pos end
if not delim_found then
error('Comma missing between object items.')
end
pos = skip_delim(str, pos, ':', true) -- true -> error if missing.
obj[key], pos = json.parse(str, pos)
pos, delim_found = skip_delim(str, pos, ',')
end
elseif first == '[' then -- Parse an array.
local arr, val, delim_found = {}, true, true
pos = pos + 1
while true do
val, pos = json.parse(str, pos, ']')
if val == nil then return arr, pos end
if not delim_found then
error('Comma missing between array items.')
end
arr[#arr + 1] = val
pos, delim_found = skip_delim(str, pos, ',')
end
elseif first == '"' then -- Parse a string.
return parse_str_val(str, pos + 1)
elseif first == '-' or first:match('%d') then -- Parse a number.
return parse_num_val(str, pos)
elseif first == end_delim then -- End of an object or array.
return nil, pos + 1
else -- Parse true, false, or null.
local literals = {
['true'] = true,
['false'] = false,
['null'] = json.null
}
for lit_str, lit_val in pairs(literals) do
local lit_end = pos + #lit_str - 1
if str:sub(pos, lit_end) == lit_str then
return lit_val, lit_end + 1
end
end
local pos_info_str = 'position ' .. pos .. ': ' ..
str:sub(pos, pos + 10)
error('Invalid json syntax starting at ' .. pos_info_str)
end
end
local function decode_jwt(jwt)
local i = 0
local result = {}
for match in (jwt .. '.'):gmatch("(.-)%.") do
result[i] = decodeBase64(match)
i = i + 1
end
-- header
local head = json.parse(result[0])
-- claims
local claims = json.parse(result[1])
return {head = head, claims = claims}
end
@TheName
Copy link

TheName commented Oct 17, 2022

Hey, could you share what's the license this is shared under? Can I use it in private project?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment