mod_unified_push: Add support for multiple token backends, including stoage
Now that we have ACLs by default, it is no longer necessary to be completely
stateless. On 0.12, using storage has benefits over JWT, because it does not
expose client JIDs to the push apps/services. In trunk, PASETO is stateless
and does not expose client JIDs.
-- phpbb3 authentication backend for Prosody
--
-- Copyright (C) 2011 Waqas Hussain
--
local log = require "util.logger".init("auth_sql");
local new_sasl = require "util.sasl".new;
local nodeprep = require "util.encodings".stringprep.nodeprep;
local saslprep = require "util.encodings".stringprep.saslprep;
local DBI = require "DBI"
local md5 = require "util.hashes".md5;
local uuid_gen = require "util.uuid".generate;
local have_bcrypt, bcrypt = pcall(require, "bcrypt"); -- available from luarocks
local connection;
local params = module:get_option("sql");
local resolve_relative_path = require "core.configmanager".resolve_relative_path;
local function test_connection()
if not connection then return nil; end
if connection:ping() then
return true;
else
module:log("debug", "Database connection closed");
connection = nil;
end
end
local function connect()
if not test_connection() then
prosody.unlock_globals();
local dbh, err = DBI.Connect(
params.driver, params.database,
params.username, params.password,
params.host, params.port
);
prosody.lock_globals();
if not dbh then
module:log("debug", "Database connection failed: %s", tostring(err));
return nil, err;
end
module:log("debug", "Successfully connected to database");
dbh:autocommit(true); -- don't run in transaction
connection = dbh;
return connection;
end
end
do -- process options to get a db connection
params = params or { driver = "SQLite3" };
if params.driver == "SQLite3" then
params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
end
assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
assert(connect());
end
local function getsql(sql, ...)
if params.driver == "PostgreSQL" then
sql = sql:gsub("`", "\"");
end
if not test_connection() then connect(); end
-- do prepared statement stuff
local stmt, err = connection:prepare(sql);
if not stmt and not test_connection() then error("connection failed"); end
if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
-- run query
local ok, err = stmt:execute(...);
if not ok and not test_connection() then error("connection failed"); end
if not ok then return nil, err; end
return stmt;
end
local function setsql(sql, ...)
local stmt, err = getsql(sql, ...);
if not stmt then return stmt, err; end
return stmt:affected();
end
local function get_password(username)
local stmt, err = getsql("SELECT `user_password` FROM `phpbb_users` WHERE `username_clean`=?", username);
if stmt then
for row in stmt:rows(true) do
return row.user_password;
end
end
end
local function check_sessionids(username, session_id)
-- TODO add session expiration and auto-login check
local stmt, err = getsql("SELECT phpbb_sessions.session_id FROM phpbb_sessions INNER JOIN phpbb_users ON phpbb_users.user_id = phpbb_sessions.session_user_id WHERE phpbb_users.username_clean =?", username);
if stmt then
for row in stmt:rows(true) do
-- if row.session_id == session_id then return true; end
-- workaround for possible LuaDBI bug
-- The session_id returned by the sql statement has an additional zero at the end. But that is not in the database.
if row.session_id == session_id or row.session_id == session_id.."0" then return true; end
end
end
end
local itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
local function hashEncode64(input, count)
local output = "";
local i, value = 0, 0;
while true do
value = input:byte(i+1)
i = i+1;
local idx = value % 0x40 + 1;
output = output .. itoa64:sub(idx, idx);
if i < count then
value = value + input:byte(i+1) * 256;
end
local _ = value % (2^6);
local idx = ((value - _) / (2^6)) % 0x40 + 1
output = output .. itoa64:sub(idx, idx);
if i >= count then break; end
i = i+1;
if i < count then
value = value + input:byte(i+1) * 256 * 256;
end
local _ = value % (2^12);
local idx = ((value - _) / (2^12)) % 0x40 + 1
output = output .. itoa64:sub(idx, idx);
if i >= count then break; end
i = i+1;
local _ = value % (2^18);
local idx = ((value - _) / (2^18)) % 0x40 + 1
output = output .. itoa64:sub(idx, idx);
if not(i < count) then break; end
end
return output;
end
local function hashCryptPrivate(password, genSalt)
local output = "*";
if not genSalt:match("^%$H%$") then return output; end
local count_log2 = itoa64:find(genSalt:sub(4,4)) - 1;
if count_log2 < 7 or count_log2 > 30 then return output; end
local count = 2 ^ count_log2;
local salt = genSalt:sub(5, 12);
if #salt ~= 8 then return output; end
local hash = md5(salt..password);
while true do
hash = md5(hash..password);
if not(count > 1) then break; end
count = count-1;
end
output = genSalt:sub(1, 12);
output = output .. hashEncode64(hash, 16);
return output;
end
local function hashGensaltPrivate(input)
local iteration_count_log2 = 6;
local output = "$H$";
local idx = math.min(iteration_count_log2 + 5, 30) + 1;
output = output .. itoa64:sub(idx, idx);
output = output .. hashEncode64(input, 6);
return output;
end
local function phpbbCheckHash(password, hash)
if #hash == 32 then return hash == md5(password, true); end -- legacy PHPBB2 hash
if #hash == 34 then return hashCryptPrivate(password, hash) == hash; end
if #hash == 60 and have_bcrypt then return bcrypt.verify(password, hash); end
module:log("error", "Unsupported hash: %s", hash);
return false;
end
local function phpbbCreateHash(password)
local random = uuid_gen():sub(-6);
local salt = hashGensaltPrivate(random);
local hash = hashCryptPrivate(password, salt);
if #hash == 34 then return hash; end
return md5(password, true);
end
provider = {};
function provider.test_password(username, password)
local hash = get_password(username);
return hash and phpbbCheckHash(password, hash);
end
function provider.user_exists(username)
module:log("debug", "test user %s existence", username);
return get_password(username) and true;
end
function provider.get_password(username)
return nil, "Getting password is not supported.";
end
function provider.set_password(username, password)
local hash = phpbbCreateHash(password);
local stmt, err = setsql("UPDATE `phpbb_users` SET `user_password`=? WHERE `username_clean`=?", hash, username);
return stmt and true, err;
end
function provider.create_user(username, password)
return nil, "Account creation/modification not supported.";
end
local escapes = {
[" "] = "\\20";
['"'] = "\\22";
["&"] = "\\26";
["'"] = "\\27";
["/"] = "\\2f";
[":"] = "\\3a";
["<"] = "\\3c";
[">"] = "\\3e";
["@"] = "\\40";
["\\"] = "\\5c";
};
local unescapes = {};
for k,v in pairs(escapes) do unescapes[v] = k; end
local function jid_escape(s) return s and (s:gsub(".", escapes)); end
local function jid_unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end
function provider.get_sasl_handler()
local sasl = {};
function sasl:clean_clone() return provider.get_sasl_handler(); end
function sasl:mechanisms() return { PLAIN = true; }; end
function sasl:select(mechanism)
if not self.selected and mechanism == "PLAIN" then
self.selected = mechanism;
return true;
end
end
function sasl:process(message)
if not message then return "failure", "malformed-request"; end
local authorization, authentication, password = message:match("^([^%z]*)%z([^%z]+)%z([^%z]+)");
if not authorization then return "failure", "malformed-request"; end
authentication = saslprep(authentication);
password = saslprep(password);
if (not password) or (password == "") or (not authentication) or (authentication == "") then
return "failure", "malformed-request", "Invalid username or password.";
end
local function test(authentication)
local prepped = nodeprep(authentication);
local normalized = jid_unescape(prepped);
return normalized and provider.test_password(normalized, password) and prepped;
end
local username = test(authentication) or test(jid_escape(authentication));
if not username and params.sessionid_as_password then
local function test(authentication)
local prepped = nodeprep(authentication);
local normalized = jid_unescape(prepped);
return normalized and check_sessionids(normalized, password) and prepped;
end
username = test(authentication) or test(jid_escape(authentication));
end
if username then
self.username = username;
return "success";
end
return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent.";
end
return sasl;
end
module:provides("auth", provider);