102 lines
3.3 KiB
Lua
102 lines
3.3 KiB
Lua
-- auth.lua — password-based session authentication using argon2id.
|
|
--
|
|
-- Sessions are stored in the `sessions` SQLite table so they survive
|
|
-- Redbean's per-request fork model (each request gets a fresh process,
|
|
-- so in-memory tables cannot be shared between requests).
|
|
--
|
|
-- The admin password hash is stored in the `settings` table under the
|
|
-- key "admin_password_hash". Use setup_admin.lua to set it.
|
|
|
|
local db = require "db"
|
|
|
|
local M = {}
|
|
|
|
local SESSION_TTL = 7 * 24 * 3600 -- 7 days in seconds
|
|
|
|
-- ── Utilities ─────────────────────────────────────────────────────────
|
|
|
|
local function to_hex(s)
|
|
return (s:gsub(".", function(c) return string.format("%02x", c:byte()) end))
|
|
end
|
|
|
|
-- Generate a cryptographically random 64-char hex token (32 bytes).
|
|
local function random_token()
|
|
local okg, rnd = pcall(GetRandomBytes, 32)
|
|
if okg and rnd and #rnd == 32 then return to_hex(rnd) end
|
|
local ok, bytes = pcall(unix.getrandom, 32)
|
|
if ok and bytes and #bytes == 32 then return to_hex(bytes) end
|
|
local f = io.open("/dev/urandom", "rb")
|
|
if f then
|
|
local b = f:read(32); f:close()
|
|
if b and #b == 32 then return to_hex(b) end
|
|
end
|
|
error("no source of randomness available")
|
|
end
|
|
|
|
-- Returns the raw token if the current request carries a live DB session.
|
|
local function current_token()
|
|
local tok = GetCookie("session")
|
|
if not tok or tok == "" then return nil end
|
|
local row = db.get_session(tok)
|
|
return row and tok or nil
|
|
end
|
|
|
|
-- ── Public API ────────────────────────────────────────────────────────
|
|
|
|
--- Returns true when the current request has a valid admin session.
|
|
function M.has_session()
|
|
return current_token() ~= nil
|
|
end
|
|
|
|
--- Enforce a valid admin session.
|
|
--- Returns nil when authorised.
|
|
--- When the session is missing, sets 302 headers and returns a non-empty
|
|
--- body (" ") so that Fullmoon calls Write(), which flushes SetHeader/
|
|
--- SetStatus to the socket. (Returning "" skips Write and the headers
|
|
--- are never sent.)
|
|
function M.require_session()
|
|
if current_token() then return nil end
|
|
SetStatus(302)
|
|
SetHeader("Location", "/login")
|
|
return " "
|
|
end
|
|
|
|
--- Verify a password and, if correct, create a DB-backed session.
|
|
--- Returns: token (string), nil on success.
|
|
--- nil, error_msg on failure.
|
|
function M.login(password)
|
|
if not password or password == "" then
|
|
return nil, "Password is required."
|
|
end
|
|
|
|
local hash = db.get_setting("admin_password_hash")
|
|
if not hash or hash == "" then
|
|
return nil, "Admin account not configured. Run setup_admin.lua first."
|
|
end
|
|
|
|
local ok, result = pcall(function()
|
|
return argon2.verify(hash, password)
|
|
end)
|
|
if not ok then
|
|
Log(kLogWarn, "argon2.verify error: " .. tostring(result))
|
|
return nil, "Internal error during authentication."
|
|
end
|
|
if not result then
|
|
return nil, "Incorrect password."
|
|
end
|
|
|
|
-- Clean up stale sessions before creating a new one.
|
|
pcall(db.prune_sessions)
|
|
|
|
local tok = random_token()
|
|
db.create_session(tok, os.time() + SESSION_TTL)
|
|
return tok, nil
|
|
end
|
|
|
|
--- Invalidate a session (logout).
|
|
function M.logout(token)
|
|
if token then pcall(db.delete_session, token) end
|
|
end
|
|
|
|
return M
|