view mod_auth_wordpress/mod_auth_wordpress.lua @ 5616:59d5fc50f602

mod_http_oauth2: Implement refresh token rotation Makes refresh tokens one-time-use, handing out a new refresh token with each access token. Thus if a refresh token is stolen and used by an attacker, the next time the legitimate client tries to use the previous refresh token, it will not work and the attack will be noticed. If the attacker does not use the refresh token, it becomes invalid after the legitimate client uses it. This behavior is recommended by draft-ietf-oauth-security-topics
author Kim Alvefur <zash@zash.se>
date Sun, 23 Jul 2023 02:56:08 +0200
parents 7dbde05b48a9
children
line wrap: on
line source

-- Wordpress authentication backend for Prosody
--
-- Copyright (C) 2011 Waqas Hussain
-- Copyright (C) 2011 Kim Alvefur
--

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 connection;
local params = module:get_option("sql");
local table_prefix = module:get_option_string("wordpress_table_prefix", "wp_");

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_pass` FROM `"..table_prefix.."users` WHERE `user_login`=?", username);
	if stmt then
		for row in stmt:rows(true) do
			return row.user_pass;
		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("^%$P%$") 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 = "$P$";
	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 wordpressCheckHash(password, hash)
	if #hash == 32 then return hash == md5(password, true); end
	return #hash == 34 and hashCryptPrivate(password, hash) == hash;
end
local function wordpressCreateHash(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 wordpressCheckHash(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 = wordpressCreateHash(password);
	local stmt, err = setsql("UPDATE `"..table_prefix.."users` SET `user_pass`=? WHERE `user_login`=?", 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 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);