├── luajwt-1.3-4.rockspec ├── example.lua ├── LICENSE ├── README.md └── luajwt.lua /luajwt-1.3-4.rockspec: -------------------------------------------------------------------------------- 1 | package = "luajwt" 2 | version = "1.3-4" 3 | 4 | source = { 5 | url = "git://github.com/x25/luajwt", 6 | tag = "v1.4" 7 | } 8 | 9 | description = { 10 | summary = "JSON Web Tokens for Lua", 11 | detailed = "Very fast and compatible with pyjwt, php-jwt, ruby-jwt, node-jwt-simple and others", 12 | homepage = "https://github.com/x25/luajwt", 13 | license = "MIT " 14 | } 15 | 16 | dependencies = { 17 | "lua >= 5.1", 18 | "luacrypto >= 0.3.2-1", 19 | "lua-cjson >= 2.1.0", 20 | "lbase64 >= 20120807-3" 21 | } 22 | 23 | build = { 24 | type = "builtin", 25 | modules = { 26 | luajwt = "luajwt.lua" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local function t2s(o) 4 | if type(o) == 'table' then 5 | local s = '{ ' 6 | for k,v in pairs(o) do 7 | if type(k) ~= 'number' then k = '"'..k..'"' end 8 | s = s .. '['..k..'] = ' .. t2s(v) .. ',' 9 | end 10 | 11 | return s .. '} ' 12 | else 13 | return tostring(o) 14 | end 15 | end 16 | 17 | -- 18 | local jwt = require "luajwt" 19 | 20 | local key = "example_key" 21 | 22 | local claim = { 23 | iss = "12345678", 24 | nbf = os.time(), 25 | exp = os.time() + 3600, 26 | } 27 | 28 | local alg = "HS256" -- default alg 29 | local token, err = jwt.encode(claim, key, alg) 30 | 31 | print("Token:", token) 32 | 33 | local validate = true -- validate exp and nbf (default: true) 34 | local decoded, err = jwt.decode(token, key, validate) 35 | 36 | print("Claim:", t2s(decoded) ) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | luajwt 2 | =========== 3 | 4 | JSON Web Tokens for Lua 5 | 6 | ```bash 7 | $ sudo luarocks install --server=http://rocks.moonscript.org luajwt 8 | ``` 9 | 10 | ## Usage 11 | 12 | Basic usage: 13 | 14 | ```lua 15 | local jwt = require "luajwt" 16 | 17 | local key = "example_key" 18 | 19 | local payload = { 20 | iss = "12345678", 21 | nbf = os.time(), 22 | exp = os.time() + 3600, 23 | } 24 | 25 | -- encode 26 | local alg = "HS256" -- (default) 27 | local token, err = jwt.encode(payload, key, alg) 28 | 29 | -- token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIx(cutted)... 30 | 31 | -- decode and validate 32 | local validate = true -- validate signature, exp and nbf (default: true) 33 | local decoded, err = jwt.decode(token, key, validate) 34 | 35 | -- decoded: { ["iss"] = 12345678, ["nbf"] = 1405108000, ["exp"] = 1405181916 } 36 | 37 | -- only decode 38 | local unsafe, err = jwt.decode(token) 39 | 40 | -- unsafe: { ["iss"] = 12345678, ["nbf"] = 1405108000, ["exp"] = 1405181916 } 41 | 42 | ``` 43 | 44 | An openresty/nginx lua jwt auth example: 45 | 46 | ``` 47 | # nginx.conf 48 | location /auth { 49 | content_by_lua ' 50 | local jwt = require "luajwt" 51 | 52 | local args = ngx.req.get_uri_args(1) 53 | 54 | if not args.jwt then 55 | 56 | return ngx.say("Where is token?") 57 | end 58 | 59 | local key = "SECRET" 60 | 61 | local ok, err = jwt.decode(args.jwt, key) 62 | 63 | if not ok then 64 | 65 | return ngx.say("Error: ", err) 66 | end 67 | 68 | ngx.say("Welcome!") 69 | '; 70 | } 71 | ``` 72 | 73 | Generate token and try: 74 | 75 | ```bash 76 | $ curl your.server/auth?jwt=TOKEN 77 | ``` 78 | 79 | ## Algorithms 80 | 81 | **HMAC** 82 | 83 | * HS256 - HMAC using SHA-256 hash algorithm (default) 84 | * HS384 - HMAC using SHA-384 hash algorithm 85 | * HS512 - HMAC using SHA-512 hash algorithm 86 | 87 | ## License 88 | MIT 89 | -------------------------------------------------------------------------------- /luajwt.lua: -------------------------------------------------------------------------------- 1 | local cjson = require 'cjson' 2 | local base64 = require 'base64' 3 | local crypto = require 'crypto' 4 | 5 | local alg_sign = { 6 | ['HS256'] = function(data, key) return crypto.hmac.digest('sha256', data, key, true) end, 7 | ['HS384'] = function(data, key) return crypto.hmac.digest('sha384', data, key, true) end, 8 | ['HS512'] = function(data, key) return crypto.hmac.digest('sha512', data, key, true) end, 9 | } 10 | 11 | local alg_verify = { 12 | ['HS256'] = function(data, signature, key) return signature == alg_sign['HS256'](data, key) end, 13 | ['HS384'] = function(data, signature, key) return signature == alg_sign['HS384'](data, key) end, 14 | ['HS512'] = function(data, signature, key) return signature == alg_sign['HS512'](data, key) end, 15 | } 16 | 17 | local function b64_encode(input) 18 | local result = base64.encode(input) 19 | 20 | result = result:gsub('+','-'):gsub('/','_'):gsub('=','') 21 | 22 | return result 23 | end 24 | 25 | local function b64_decode(input) 26 | -- input = input:gsub('\n', ''):gsub(' ', '') 27 | 28 | local reminder = #input % 4 29 | 30 | if reminder > 0 then 31 | local padlen = 4 - reminder 32 | input = input .. string.rep('=', padlen) 33 | end 34 | 35 | input = input:gsub('-','+'):gsub('_','/') 36 | 37 | return base64.decode(input) 38 | end 39 | 40 | local function tokenize(str, div, len) 41 | local result, pos = {}, 0 42 | 43 | for st, sp in function() return str:find(div, pos, true) end do 44 | 45 | result[#result + 1] = str:sub(pos, st-1) 46 | pos = sp + 1 47 | 48 | len = len - 1 49 | 50 | if len <= 1 then 51 | break 52 | end 53 | end 54 | 55 | result[#result + 1] = str:sub(pos) 56 | 57 | return result 58 | end 59 | 60 | local M = {} 61 | 62 | function M.encode(data, key, alg) 63 | if type(data) ~= 'table' then return nil, "Argument #1 must be table" end 64 | if type(key) ~= 'string' then return nil, "Argument #2 must be string" end 65 | 66 | alg = alg or "HS256" 67 | 68 | if not alg_sign[alg] then 69 | return nil, "Algorithm not supported" 70 | end 71 | 72 | local header = { typ='JWT', alg=alg } 73 | 74 | local segments = { 75 | b64_encode(cjson.encode(header)), 76 | b64_encode(cjson.encode(data)) 77 | } 78 | 79 | local signing_input = table.concat(segments, ".") 80 | 81 | local signature = alg_sign[alg](signing_input, key) 82 | 83 | segments[#segments+1] = b64_encode(signature) 84 | 85 | return table.concat(segments, ".") 86 | end 87 | 88 | function M.decode(data, key, verify) 89 | if key and verify == nil then verify = true end 90 | if type(data) ~= 'string' then return nil, "Argument #1 must be string" end 91 | if verify and type(key) ~= 'string' then return nil, "Argument #2 must be string" end 92 | 93 | local token = tokenize(data, '.', 3) 94 | 95 | if #token ~= 3 then 96 | return nil, "Invalid token" 97 | end 98 | 99 | local headerb64, bodyb64, sigb64 = token[1], token[2], token[3] 100 | 101 | local ok, header, body, sig = pcall(function () 102 | 103 | return cjson.decode(b64_decode(headerb64)), 104 | cjson.decode(b64_decode(bodyb64)), 105 | b64_decode(sigb64) 106 | end) 107 | 108 | if not ok then 109 | return nil, "Invalid json" 110 | end 111 | 112 | if verify then 113 | 114 | if not header.typ or header.typ ~= "JWT" then 115 | return nil, "Invalid typ" 116 | end 117 | 118 | if not header.alg or type(header.alg) ~= "string" then 119 | return nil, "Invalid alg" 120 | end 121 | 122 | if body.exp and type(body.exp) ~= "number" then 123 | return nil, "exp must be number" 124 | end 125 | 126 | if body.nbf and type(body.nbf) ~= "number" then 127 | return nil, "nbf must be number" 128 | end 129 | 130 | if not alg_verify[header.alg] then 131 | return nil, "Algorithm not supported" 132 | end 133 | 134 | if not alg_verify[header.alg](headerb64 .. "." .. bodyb64, sig, key) then 135 | return nil, "Invalid signature" 136 | end 137 | 138 | if body.exp and os.time() >= body.exp then 139 | return nil, "Not acceptable by exp" 140 | end 141 | 142 | if body.nbf and os.time() < body.nbf then 143 | return nil, "Not acceptable by nbf" 144 | end 145 | end 146 | 147 | return body 148 | end 149 | 150 | return M 151 | --------------------------------------------------------------------------------