-- Modified copy of Prosody's mod_auth_internal_hashed (MIT/X11, see Prosody's COPYING). -- Selected with AUTH_TYPE=openfire (authentication = "openfire_hybrid"); it -- accepts local accounts and also accounts that exist in Openfire. Local credentials are tried first; on failure the password is -- verified against Openfire (direct-TLS c2s, SASL PLAIN, certificate and -- hostname verified). A local record is created on the first success. -- Settings come from the environment: OPENFIRE_AUTH_ENABLED, OPENFIRE_AUTH_HOST, -- OPENFIRE_AUTH_PORT, OPENFIRE_AUTH_DOMAIN, OPENFIRE_AUTH_EXCLUDE. -- Only SASL PLAIN is offered, because an external check cannot do SCRAM. -- -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2010 Jeff Mitchell -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local max = math.max; local scram_hashers = require "prosody.util.sasl.scram".hashers; local generate_uuid = require "prosody.util.uuid".generate; local new_sasl = require "prosody.util.sasl".new; local hex = require"prosody.util.hex"; local to_hex, from_hex = hex.encode, hex.decode; local saslprep = require "prosody.util.encodings".stringprep.saslprep; local secure_equals = require "prosody.util.hashes".equals; local log = module._log; local host = module.host; local accounts = module:open_store("accounts"); local hash_name = module:get_option_enum("password_hash", "SHA-1", "SHA-256"); local get_auth_db = assert(scram_hashers[hash_name], "SCRAM-"..hash_name.." not supported by SASL library"); local scram_name = "scram_"..hash_name:gsub("%-","_"):lower(); -- Default; can be set per-user local default_iteration_count = module:get_option_integer("default_iteration_count", 10000, 4096); -- define auth provider local provider = {}; local function local_test_password(username, password) log("debug", "test password for user '%s'", username); local credentials = accounts:get(username) or {}; if credentials.disabled then return nil, "Account disabled."; end password = saslprep(password); if not password then return nil, "Password fails SASLprep."; end if credentials.password ~= nil and string.len(credentials.password) ~= 0 then if not secure_equals(saslprep(credentials.password), password) then return nil, "Auth failed. Provided password is incorrect."; end if provider.set_password(username, credentials.password) == nil then return nil, "Auth failed. Could not set hashed password from plaintext."; else return true; end end if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then return nil, "Auth failed. Stored salt and iteration count information is not complete."; end local valid, stored_key, server_key = get_auth_db(password, credentials.salt, credentials.iteration_count); local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); if valid and secure_equals(stored_key_hex, credentials.stored_key) and secure_equals(server_key_hex, credentials.server_key) then return true; else return nil, "Auth failed. Invalid username, password, or password hash information."; end end local socket = require "socket"; local ssl = require "ssl"; local b64 = require "prosody.util.encodings".base64; local x509 = require "prosody.util.x509"; local of_enabled = (os.getenv("OPENFIRE_AUTH_ENABLED") or "0"):lower(); of_enabled = of_enabled == "1" or of_enabled == "true" or of_enabled == "yes"; local of_host = os.getenv("OPENFIRE_AUTH_HOST") or "172.17.0.1"; local of_port = tonumber(os.getenv("OPENFIRE_AUTH_PORT") or "5223"); local of_domain = os.getenv("OPENFIRE_AUTH_DOMAIN") or ""; if of_domain == "" then of_enabled = false; end local of_excluded = {}; for name in (os.getenv("OPENFIRE_AUTH_EXCLUDE") or "focus,jvb,jibri,recorder"):gmatch("[^,%s]+") do of_excluded[name:lower()] = true; end local function xml_escape(text) return (text:gsub("[&<>'\"]", { ["&"] = "&", ["<"] = "<", [">"] = ">", ["'"] = "'", ['"'] = """ })); end local function read_until(sock, needles, limit) local buf = ""; while #buf < limit do for _, needle in ipairs(needles) do if buf:find(needle, 1, true) then return buf, needle; end end local ch = sock:receive(1); if not ch then return buf, nil; end buf = buf .. ch; end return buf, nil; end local function openfire_check(username, password) local tcp = socket.tcp(); tcp:settimeout(5); local connected, cerr = tcp:connect(of_host, of_port); if not connected then log("warn", "Openfire auth: cannot connect to %s:%d: %s", of_host, of_port, tostring(cerr)); return nil; end local sock = ssl.wrap(tcp, { mode = "client", protocol = "any", options = { "all", "no_sslv2", "no_sslv3", "no_tlsv1", "no_tlsv1_1" }, verify = { "peer" }, verifyext = { "lsec_continue", "lsec_ignore_purpose" }, cafile = "/etc/ssl/certs/ca-certificates.crt", }); sock:sni(of_domain); sock:settimeout(5); local hs_ok, hs_err = sock:dohandshake(); if not hs_ok then log("warn", "Openfire auth: TLS handshake failed: %s", tostring(hs_err)); sock:close(); return nil; end local cert = sock:getpeercertificate(); if not cert or not x509.verify_identity(of_domain, "xmpp-client", cert) then log("warn", "Openfire auth: certificate does not match %s", of_domain); sock:close(); return nil; end sock:send(""); if not read_until(sock, { "" }, 16384) then sock:close(); return nil; end local token = b64.encode("\0" .. username .. "\0" .. password); sock:send("" .. token .. ""); local _, found = read_until(sock, { ""); sock:close(); return found == "