From c38a2b99de77abeb9e1adfa753a35903c8195f6f Mon Sep 17 00:00:00 2001 From: the lemons Date: Sun, 9 Apr 2023 00:30:09 -0500 Subject: initial commit --- LICENSE.txt | 19 +++++++ Makefile | 7 +++ auth.lua | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 auth.lua diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..11c2dcf --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright © 2023 citrons + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a8f58df --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +LUA_VER=5.3 +PREFIX=/usr/local +INSTALL_DIR=$(PREFIX)/lib/lua/$(LUA_VER) + +install: auth.lua + mkdir -m755 -p $(INSTALL_DIR)/citrons + install -m755 auth.lua $(INSTALL_DIR)/citrons/auth.lua diff --git a/auth.lua b/auth.lua new file mode 100644 index 0000000..978fbc4 --- /dev/null +++ b/auth.lua @@ -0,0 +1,179 @@ +local request = require 'http.request' +local util = require 'http.util' +local json = require 'json' + +local M = {} +M.__index = M + +M.cache_enabled = true +M.tmpdir = "/tmp" +M.api_domain = "auth.citrons.xyz" +M.timeout = 8 + +local db +local function txn(write_enabled) + local lmdb = require 'lmdb' + if not lmdb then + return + end + if not db then + local user = os.getenv "USER" and "."..os.getenv "USER" or "" + db = lmdb.open(M.tmpdir.."/"..M.api_domain..user..".mdb", { + nosubdir = true, mapsize = 2^20 * 5, maxdbs = 256, + }) + end + if not db then return end + return db:txn_begin(write_enabled) +end + +local function cache_get(name, k) + local t = txn() + local d = t:open(name) + local entry + if d then + entry = d[k] and json.decode(d[k]) or nil + end + t:commit() + if entry and os.time() <= entry.expires then + return true, entry.v + end +end + +local function cache_clear(name, k) + local t = txn(true) + local d = t:open(name, true) + if d[k] then d[k] = nil end +end + +local function cache_put(name, k, v) + local t = txn(true) + local d = t:open(name, true) + if d then + local entry = {v = v} + if type(v) == 'table' and v.ttl then + entry.expires = os.time() + v.ttl + else + entry.expires = os.time() + 300 + end + d[k] = json.encode(entry) + end + t:commit() + return v +end + +local rq +local default_headers +local function get_rq(path) + if not rq then + rq = request.new_from_uri("https://"..M.api_domain) + default_headers = rq.headers + end + rq.headers = default_headers:clone() + rq.headers:upsert(':path', path) + rq:set_body(nil) + return rq +end + +local function api_get(path) + local rq = get_rq(path) + local headers, stream = assert(rq:go(M.timeout)) + if headers:get ':status' == "404" then + return nil + end + assert(headers:get ':status' == "200", headers:get ':status') + local data = assert(stream:get_body_as_string(M.timeout)) + return json.decode(data) +end + +local function api_post(path, form) + local rq = get_rq(path) + rq.headers:upsert(':method', 'POST') + rq.headers:upsert('content-type', 'application/x-www-form-urlencoded') + rq:set_body(util.dict_to_query(form)) + local headers, stream = assert(rq:go(M.timeout)) + if headers:get ':status' == "404" then + return nil + end + assert(headers:get ':status' == "200", headers:get ':status') + local data = assert(stream:get_body_as_string(M.timeout)) + return json.decode(data) +end + +function M.login_url(service) + return "https://"..M.api_domain.."/login?" + ..util.dict_to_query {service = service} +end + +function M.service_lookup(domain) + if not domain:match "^[%w_%-%.]+$" then + return nil, "invalid service name!" + end + domain = domain:gsub('%.+', ".") + + local _, data = cache_get('services', domain) + if data then return data end + + local meta_uri = "https://"..domain.."/.well-known/citrons/auth" + local rq = request.new_from_uri(meta_uri) + local headers, stream = rq:go(M.timeout) + if not headers then return nil, stream end + if headers:get ":status" ~= "200" then + return nil, "HTTP error: "..headers:get ":status" + end + local data, err = stream:get_body_chars(4096, 4) + if not data then return nil, err end + local ok, result = pcall(json.decode, data) + if not ok then return nil, "could not decode JSON" end + + cache_put('services', domain, result) + + return result +end + +local users = setmetatable({}, {__mode = 'k'}) +function M.user(uid) + if not users[uid] then + local u = setmetatable({uid = uid}, M) + if u:get_data() then + users[uid] = u + end + end + return users[uid] +end + +function M:get_data() + assert( + type(self.uid) == 'string' and self.uid:match '^%w+$', "invalid uid") + local cached, data = cache_get('users', self.uid) + if cached then return data end + data = api_get('/api/user/'..self.uid) + cache_put('users', self.uid, data) + return data +end + +function M:username() + return self:get_data().username +end + +function M:authenticate(service, token) + local cached, data = cache_get('tokens', token) + if not cached then + data = api_post( + '/api/user/'..self.uid..'/auth/'..service, {token = token}) + cache_put('tokens', token, data) + cache_put('users', self.uid, data.user) + end + if not data then return false end + return data.valid +end + +function M:invalidate(token) + local data = api_post( + '/api/user/'..self.uid..'/invalidate', {token = token}) + if data then + cache_put('users', self.uid, data) + end + cache_put('tokens', token, nil) +end + +return M -- cgit v1.2.3