snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 15:56:32 +05:00
commit 1d2bfcac0c
3159 changed files with 464524 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
-- Enforces JWT token room-claim verification at MUC join/create time. Loaded
-- on the MUC component. For each join or room-create, calls
-- token_util:verify_room() to check that the token's room claim matches the
-- target room JID. Admins and domains listed in token_verification_allowlist
-- are exempt. Anonymous users (no token) are allowed through. When
-- token_verification_require_token_for_moderation is set, also blocks room
-- config IQs (e.g. granting moderator status) from unauthenticated users.
-- Records the verified conference on the session (jitsi_meet_verified_room) and
-- answers the 'jitsi-verify-session-rooms' event, so that a session whose JWT
-- claims are refreshed after the join (mod_auth_token, on stream resumption) is
-- re-checked against it.
-- Token authentication
-- Copyright (C) 2021-present 8x8, Inc.
local log = module._log;
local host = module.host;
local st = require "util.stanza";
local jid_split = require 'util.jid'.split;
local jid_bare = require 'util.jid'.bare;
local util = module:require 'util';
local is_admin = util.is_admin;
local DEBUG = false;
local measure_success = module:measure('success', 'counter');
local measure_fail = module:measure('fail', 'counter');
local parentHostName = string.gmatch(tostring(host), "%w+.(%w.+)")();
if parentHostName == nil then
module:log("error", "Failed to start - unable to get parent hostname");
return;
end
local parentCtx = module:context(parentHostName);
if parentCtx == nil then
module:log("error",
"Failed to start - unable to get parent context for host: %s",
tostring(parentHostName));
return;
end
local token_util = module:require "token/util".new(parentCtx);
-- no token configuration
if token_util == nil then
return;
end
module:log("debug",
"%s - starting MUC token verifier app_id: %s app_secret: %s allow empty: %s",
tostring(host), tostring(token_util.appId), tostring(token_util.appSecret),
tostring(token_util.allowEmptyToken));
-- option to disable room modification (sending muc config form) for guest that do not provide token
local require_token_for_moderation;
-- option to allow domains to skip token verification
local allowlist;
local function load_config()
require_token_for_moderation = module:get_option_boolean("token_verification_require_token_for_moderation");
allowlist = module:get_option_set('token_verification_allowlist', {});
end
load_config();
-- verify user and whether he is allowed to join a room based on the token information
local function verify_user(session, stanza)
if DEBUG then
module:log("debug", "Session token: %s, session room: %s",
tostring(session.auth_token), tostring(session.jitsi_meet_room));
end
-- token not required for admin users
local user_jid = stanza.attr.from;
if is_admin(user_jid) then
if DEBUG then module:log("debug", "Token not required from admin user: %s", user_jid); end
return true;
end
-- token not required for users matching allow list
local user_bare_jid = jid_bare(user_jid);
local _, user_domain = jid_split(user_jid);
-- allowlist for participants, jigasi (sip & transcriber), jibri (recorder & sip)
if allowlist:contains(user_domain)
or allowlist:contains(user_bare_jid)
-- allow main participants in visitor mode
or session.type == 's2sin' then
if DEBUG then module:log("debug", "Token not required from user in allow list: %s", user_jid); end
return true;
end
if DEBUG then module:log("debug", "Will verify token for user: %s, room: %s ", user_jid, stanza.attr.to); end
local res, err, reason = token_util:verify_room(session, stanza.attr.to);
if not res then
if not err and not reason then
reason = 'Room and token mismatched';
end
module:log('error', 'Token %s not allowed to join: %s err: %s reason: %s',
tostring(session.auth_token), tostring(stanza.attr.to), err, reason);
local response = st.error_reply(stanza, 'cancel', 'not-allowed', reason);
if err then
response:tag(err, { xmlns = 'http://jitsi.org/jitmeet' });
end
session.send(response);
return false; -- we need to just return non nil
end
if DEBUG then module:log("debug", "allowed: %s to enter/create room: %s", user_jid, stanza.attr.to); end
-- Remember the conference this session was verified for, so claims that are
-- refreshed later (mod_auth_token, on stream resumption) can be checked
-- against it without going through a join again.
session.jitsi_meet_verified_room = jid_bare(stanza.attr.to);
return true;
end
module:hook("muc-room-pre-create", function(event)
local origin, stanza = event.origin, event.stanza;
if DEBUG then module:log("debug", "pre create: %s %s", tostring(origin), tostring(stanza)); end
if not verify_user(origin, stanza) then
measure_fail(1);
return true; -- Returning any value other than nil will halt processing of the event
end
measure_success(1);
end, 99);
module:hook("muc-occupant-pre-join", function(event)
local origin, room, stanza = event.origin, event.room, event.stanza;
if DEBUG then module:log("debug", "pre join: %s %s", tostring(room), tostring(stanza)); end
if not verify_user(origin, stanza) then
measure_fail(1);
return true; -- Returning any value other than nil will halt processing of the event
end
measure_success(1);
end, 99);
-- Verifies the token claims currently on a session against the conference it
-- was verified for on join. Fired by mod_auth_token when a connection resuming
-- a hibernating session (XEP-0198) presents a different token, which refreshes
-- the claims of the live session: the hooks above only run at join time, so
-- this applies the same room check to a session that is already in a room.
-- Answering with res=false makes mod_auth_token keep the claims that were
-- verified on join.
module:hook_global('jitsi-verify-session-rooms', function(event)
local session = event.session;
local room_jid = session.jitsi_meet_verified_room;
if not room_jid then
return;
end
-- Being an occupant is deliberately not required: a session that moved into
-- a breakout room has left the main room it joined, and that main room is
-- still the conference its claims have to cover. Only a room that is gone
-- (everyone left while the session was hibernating) is skipped.
if not prosody.hosts[host].modules.muc.get_room_from_jid(room_jid) then
return;
end
local res, err, reason = token_util:verify_room(session, room_jid);
if not res then
measure_fail(1);
return { res = false; room = room_jid; error = err; reason = reason; };
end
measure_success(1);
end);
for event_name, method in pairs {
-- Normal room interactions
["iq-set/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
-- Host room
["iq-set/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
} do
module:hook(event_name, function (event)
local session, stanza = event.origin, event.stanza;
-- if we do not require token we pass it through(default behaviour)
-- or the request is coming from admin (focus)
if not require_token_for_moderation or is_admin(stanza.attr.from) then
return;
end
-- jitsi_meet_room is set after the token had been verified
if not session.auth_token or not session.jitsi_meet_room then
session.send(
st.error_reply(
stanza, "cancel", "not-allowed", "Room modification disabled for guests"));
return true;
end
end, -1); -- the default prosody hook is on -2
end
module:hook_global('config-reloaded', load_config);