aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKim Alvefur <zash@zash.se>2020-02-24 01:24:25 +0100
committerKim Alvefur <zash@zash.se>2020-02-24 01:24:25 +0100
commit0bcbbed75359c7460463e6f33b81a74dcb1b801f (patch)
treeca0a816da0312b741fa33f69b38aad3faedd7dbf
parent8d04879adfbe5d4039a14c5bd10e95ee4b051566 (diff)
downloadprosody-0bcbbed75359c7460463e6f33b81a74dcb1b801f.tar.gz
prosody-0bcbbed75359c7460463e6f33b81a74dcb1b801f.zip
util.jwt: Basic JSON Web Token library supporting HS256 tokens
-rw-r--r--spec/util_jwt_spec.lua20
-rw-r--r--util/jwt.lua50
2 files changed, 70 insertions, 0 deletions
diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua
new file mode 100644
index 00000000..5e688f7b
--- /dev/null
+++ b/spec/util_jwt_spec.lua
@@ -0,0 +1,20 @@
+local jwt = require "util.jwt";
+
+describe("util.jwt", function ()
+ it("validates", function ()
+ local key = "secret";
+ local token = jwt.sign(key, { payload = "this" });
+ assert.string(token);
+ local ok, parsed = jwt.verify(key, token);
+ assert.truthy(ok)
+ assert.same({ payload = "this" }, parsed);
+ end);
+ it("rejects invalid", function ()
+ local key = "secret";
+ local token = jwt.sign("wrong", { payload = "this" });
+ assert.string(token);
+ local ok, err = jwt.verify(key, token);
+ assert.falsy(ok)
+ end);
+end);
+
diff --git a/util/jwt.lua b/util/jwt.lua
new file mode 100644
index 00000000..2b172d38
--- /dev/null
+++ b/util/jwt.lua
@@ -0,0 +1,50 @@
+local s_gsub = string.gsub;
+local json = require "util.json";
+local hashes = require "util.hashes";
+local base64_encode = require "util.encodings".base64.encode;
+local base64_decode = require "util.encodings".base64.decode;
+
+local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" };
+local function b64url(data)
+ return (s_gsub(base64_encode(data), "[+/=]", b64url_rep));
+end
+local function unb64url(data)
+ return base64_decode(s_gsub(data, "[-_]", b64url_rep).."==");
+end
+
+local static_header = b64url('{"alg":"HS256","typ":"JWT"}') .. '.';
+
+local function sign(key, payload)
+ local encoded_payload = json.encode(payload);
+ local signed = static_header .. b64url(encoded_payload);
+ local signature = hashes.hmac_sha256(key, signed);
+ return signed .. "." .. b64url(signature);
+end
+
+local jwt_pattern = "^(([A-Za-z0-9-_]+)%.([A-Za-z0-9-_]+))%.([A-Za-z0-9-_]+)$"
+local function verify(key, blob)
+ local signed, bheader, bpayload, signature = string.match(blob, jwt_pattern);
+ if not signed then
+ return nil, "invalid-encoding";
+ end
+ local header = json.decode(unb64url(bheader));
+ if not header or type(header) ~= "table" then
+ return nil, "invalid-header";
+ elseif header.alg ~= "HS256" then
+ return nil, "unsupported-algorithm";
+ end
+ if b64url(hashes.hmac_sha256(key, signed)) ~= signature then
+ return false, "signature-mismatch";
+ end
+ local payload, err = json.decode(unb64url(bpayload));
+ if err ~= nil then
+ return nil, "json-decode-error";
+ end
+ return true, payload;
+end
+
+return {
+ sign = sign;
+ verify = verify;
+};
+