From 1cf79e73256603be515ca13c7da714c993543dee Mon Sep 17 00:00:00 2001 From: casjay Date: Sun, 8 Nov 2026 10:19:18 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Openfire=20auth=20option,=20f?= =?UTF-8?q?ix=20compose=20and=20docs=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker/rootfs/usr/local/bin/entrypoint.sh: honor AUTH_TYPE (internal|openfire) for the global and main-host authentication module; reject unknown values - docker/rootfs/prosody-plugins-contrib/mod_auth_openfire_hybrid.lua: new auth module, local accounts then Openfire over verified direct TLS, disabled unless OPENFIRE_AUTH_DOMAIN is set - docker/docker-compose.yml: renamed to docker/compose.yml, map-style env, healthcheck, logging, IPv6 network - README.md: document the authentication variables --- README.md | 13 + docker/compose.yml | 56 ++++ docker/docker-compose.yml | 29 -- .../mod_auth_openfire_hybrid.lua | 277 ++++++++++++++++++ docker/rootfs/usr/local/bin/entrypoint.sh | 16 +- 5 files changed, 359 insertions(+), 32 deletions(-) create mode 100644 docker/compose.yml delete mode 100644 docker/docker-compose.yml create mode 100644 docker/rootfs/prosody-plugins-contrib/mod_auth_openfire_hybrid.lua diff --git a/README.md b/README.md index b0e756c..de90a4c 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,16 @@ buildx 🤖 casjay: [Github](https://github.com/casjay) 🤖 ⛵ casjaysdevdocker: [Github](https://github.com/casjaysdevdocker) [Docker](https://hub.docker.com/u/casjaysdevdocker) ⛵ + +## Authentication + +| Variable | Default | Meaning | +|---|---|---| +| `AUTH_TYPE` | `internal` | `internal` = local accounts only; `openfire` = local accounts, then Openfire | +| `OPENFIRE_AUTH_HOST` | `172.17.0.1` | Openfire direct-TLS c2s host (certificate and hostname verified) | +| `OPENFIRE_AUTH_PORT` | `5223` | Openfire direct-TLS c2s port | +| `OPENFIRE_AUTH_DOMAIN` | empty | Openfire XMPP domain (required for `AUTH_TYPE=openfire`) | +| `OPENFIRE_AUTH_EXCLUDE` | `focus,jvb,jibri,recorder` | Service accounts that never use Openfire | + +Service accounts (`focus`, `jvb`, `jibri`, `jigasi`) always use local `internal_hashed`. +Only SASL PLAIN is offered with `openfire`, since an external check cannot do SCRAM. diff --git a/docker/compose.yml b/docker/compose.yml new file mode 100644 index 0000000..4a67b5e --- /dev/null +++ b/docker/compose.yml @@ -0,0 +1,56 @@ +# nginx proxy address - http://172.17.0.1:5280 +name: prosody + +x-logging: &logging + driver: json-file + options: + max-size: 10m + max-file: "3" + +services: + prosody: + image: ghcr.io/casjaysdevdocker/prosody:latest + pull_policy: always + restart: always + logging: *logging + environment: + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_GUEST_DOMAIN: guest.meet.jitsi + XMPP_MUC_DOMAIN: muc.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + XMPP_HIDDEN_DOMAIN: hidden.meet.jitsi + JICOFO_AUTH_PASSWORD: ${JICOFO_AUTH_PASSWORD:?set in .env} + JVB_AUTH_PASSWORD: ${JVB_AUTH_PASSWORD:?set in .env} + ENABLE_AUTH: "0" + ENABLE_GUESTS: "0" + ENABLE_XMPP_WEBSOCKET: "1" + LOG_LEVEL: info + # internal = local accounts only; openfire = also accept Openfire accounts + AUTH_TYPE: internal + OPENFIRE_AUTH_ENABLED: "0" + OPENFIRE_AUTH_HOST: 172.17.0.1 + OPENFIRE_AUTH_PORT: "5223" + OPENFIRE_AUTH_DOMAIN: "" + OPENFIRE_AUTH_EXCLUDE: focus,jvb,jibri,recorder + volumes: + - ./volumes/config:/config + ports: + - "5222:5222" + - "5280:5280" + - "5347:5347" + networks: + - prosody + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck.sh"] + interval: 30s + timeout: 10s + retries: 3 + +networks: + prosody: + driver: bridge + enable_ipv6: true + ipam: + config: + - subnet: fd00:5a:30::/64 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 2a2d507..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -services: - prosody: - image: ghcr.io/casjaysdevdocker/prosody:latest - restart: unless-stopped - environment: - - XMPP_DOMAIN=meet.jitsi - - XMPP_AUTH_DOMAIN=auth.meet.jitsi - - XMPP_GUEST_DOMAIN=guest.meet.jitsi - - XMPP_MUC_DOMAIN=muc.meet.jitsi - - XMPP_INTERNAL_MUC_DOMAIN=internal-muc.meet.jitsi - - XMPP_HIDDEN_DOMAIN=hidden.meet.jitsi - - JICOFO_AUTH_PASSWORD=changeme_jicofo - - JVB_AUTH_PASSWORD=changeme_jvb - - ENABLE_AUTH=0 - - ENABLE_GUESTS=0 - - ENABLE_XMPP_WEBSOCKET=1 - - LOG_LEVEL=info - volumes: - - ./volumes/config:/config - ports: - - "5222:5222" - - "5280:5280" - - "5347:5347" - networks: - - prosody-net - -networks: - prosody-net: - driver: bridge diff --git a/docker/rootfs/prosody-plugins-contrib/mod_auth_openfire_hybrid.lua b/docker/rootfs/prosody-plugins-contrib/mod_auth_openfire_hybrid.lua new file mode 100644 index 0000000..3b90337 --- /dev/null +++ b/docker/rootfs/prosody-plugins-contrib/mod_auth_openfire_hybrid.lua @@ -0,0 +1,277 @@ +-- 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 == " internal_hashed (local accounts only) +# openfire -> openfire_hybrid (local accounts, then Openfire via OPENFIRE_AUTH_*) +case "${AUTH_TYPE}" in + internal) USER_AUTH_MODULE="internal_hashed" ;; + openfire) USER_AUTH_MODULE="openfire_hybrid" ;; + *) echo "FATAL: AUTH_TYPE must be internal or openfire (got: ${AUTH_TYPE})" >&2; exit 1 ;; +esac XMPP_MUC_DOMAIN_PREFIX="${XMPP_MUC_DOMAIN%%.*}" PROSODY_CFG="/config/prosody.cfg.lua" PROSODY_SITE_CFG="/config/conf.d/jitsi-meet.cfg.lua" @@ -138,7 +148,7 @@ c2s_interfaces = { "*" } s2s_secure_auth = false -authentication = "internal_hashed" +authentication = "${USER_AUTH_MODULE}" -- Logging log = { @@ -184,7 +194,7 @@ __generate_site_cfg() { # Determine main VirtualHost authentication local main_auth="jitsi-anonymous" if [[ "${ENABLE_AUTH}" == "1" ]] || [[ "${ENABLE_AUTH}" == "true" ]]; then - main_auth="internal_hashed" + main_auth="${USER_AUTH_MODULE}" fi # Determine guest VirtualHost authentication