summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorthe lemons <citrons@mondecitronne.com>2023-04-09 00:30:09 -0500
committerthe lemons <citrons@mondecitronne.com>2023-04-09 00:30:09 -0500
commitc38a2b99de77abeb9e1adfa753a35903c8195f6f (patch)
tree89d5053989bf826a7922b148dca7bf53bfa32a65
initial commit
-rw-r--r--LICENSE.txt19
-rw-r--r--Makefile7
-rw-r--r--auth.lua179
3 files changed, 205 insertions, 0 deletions
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