Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and [Matrix user verification service](https://github.com/matrix-org/matrix-user
to handle verifying a given Matrix user exists and that they are in a room that
matches the Jitsi room ID.

Additionally, can make the verified user an owner of the conference if power level
syncing has been turned on.

## Flow diagrams

These diagrams explain how the different components fit together around this Prosody module.
Expand Down Expand Up @@ -99,6 +102,13 @@ VirtualHost "example.com"
-- (optional) UVS auth token, if authentication enabled
-- Uncomment and set the right token if necessary
--uvs_auth_token = "changeme"
-- (optional) Make Matrix room moderators owners of the Prosody room.
-- Enabling this will mean once a participant, authed using this module,
-- joins a call, their power in the relevant Matrix room will be checked
-- via UVS and if they have more or equal the configured power here,
-- they will be made an owner of the Prosody room.
-- This is disabled by default, uncomment with a sufficient level below.
--uvs_sync_power_levels = true
```

The prosody image needs to have the Lua module `http` installed. Install it with LuaRocks:
Expand Down
4 changes: 3 additions & 1 deletion diagram_sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ Synapse->UVS: Verification response\nreturns Matrix ID
UVS->Synapse: Check user in the room\n(Synapse admin API)
Synapse->UVS: Verification response

UVS->Jitsi: Authentication response\n(returns Matrix ID, if needed)
UVS->Jitsi: Authentication response\n(returns also Matrix ID and room power levels)

Jitsi->Widget: Join the conference

Jitsi->Jitsi: Make participant conference\nowner if power levels match
```
112 changes: 107 additions & 5 deletions mod_auth_matrix_user_verification.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

local formdecode = require "util.http".formdecode;
local generate_uuid = require "util.uuid".generate;
local jid = require "util.jid";
local jwt = require "luajwtjitsi";
local new_sasl = require "util.sasl".new;
local sasl = require "util.sasl";
local sessions = prosody.full_sessions;
local basexx = require "basexx";
local http_request = require "http.request";
local json = require "util.json";
local um_is_admin = require "core.usermanager".is_admin;

-- Ensure configured
local uvsUrl = module:get_option("uvs_base_url", nil);
Expand All @@ -22,6 +24,7 @@ else
module:log("info", string.format("uvs_base_url = %s", uvsUrl));
end
local uvsAuthToken = module:get_option("uvs_auth_token", nil);
local uvsSyncPowerLevels = module:get_option("uvs_sync_power_levels", false);

-- define auth provider
local provider = {};
Expand Down Expand Up @@ -52,6 +55,95 @@ end
module:hook_global("bosh-session", init_session);
module:hook_global("websocket-session", init_session);

-- Source: https://github.com/jitsi/jitsi-meet/blob/master/resources/prosody-plugins/util.lib.lua#L248
local function starts_with(str, start)
return str:sub(1, #start) == start
end

--- Extracts the subdomain and room name from internal jid node [foo]room1
-- @return subdomain(optional, if extracted or nil), the room name
-- Source: https://github.com/jitsi/jitsi-meet/blob/master/resources/prosody-plugins/util.lib.lua#L239
local function extract_subdomain(room_node)
-- optimization, skip matching if there is no subdomain, no [subdomain] part in the beginning of the node
if not starts_with(room_node, '[') then
return nil, room_node;
end

return room_node:match("^%[([^%]]+)%](.+)$");
end

-- healthcheck rooms in jicofo starts with a string '__jicofo-health-check'
-- Source: https://github.com/jitsi/jitsi-meet/blob/master/resources/prosody-plugins/util.lib.lua#L253
local function is_healthcheck_room(room_jid)
if starts_with(room_jid, "__jicofo-health-check") then
return true;
end

return false;
end

-- Mostly taken from: https://github.com/jitsi/jitsi-meet/blob/master/resources/prosody-plugins/mod_muc_allowners.lua#L63
local function should_sync_power_level(room, occupant)
if is_healthcheck_room(room.jid) or um_is_admin(occupant.jid, host) then
return false;
end

local room_node = jid.node(room.jid);
-- parses bare room address, for multidomain expected format is:
-- [subdomain]roomName@conference.domain
local target_subdomain, target_room_name = extract_subdomain(room_node);

if not (target_room_name == session.jitsi_meet_room) then
module:log(
'debug',
'skip power level sync for auth user and non matching room name: %s, jwt room name: %s',
target_room_name,
session.jitsi_meet_room
);
return false;
end

if not (target_subdomain == session.jitsi_meet_context_group) then
module:log(
'debug',
'skip power level sync for auth user and non matching room subdomain: %s, jwt subdomain: %s',
target_subdomain,
session.jitsi_meet_context_group
);
return false;
end

return true;
end

module:hook("muc-occupant-joined", function (event)
if not uvsSyncPowerLevels then
-- Nothing to do, power level syncing is not enabled.
return;
end

local room = event.room;
local occupant = event.occupant;

-- Check if we want to sync power level for this room
if not should_sync_power_level(room, occupant) then
return;
end

local session = event.origin;
if session.auth_matrix_user_verification_is_owner ~= true then
module:log(
'debug',
'skip setting power levels, user is not authed or is not marked as owner of room: %s',
room.jid
);
return;
end

-- Otherwise, set user owner
room:set_affiliation(true, occupant.bare_jid, "owner");
end, 2);

function provider.test_password(username, password)
return nil, "Password based auth not supported";
end
Expand All @@ -76,6 +168,8 @@ function provider.delete_user(username)
return nil;
end

-- Check room membership from UVS
-- Returns boolean(isMember), boolean(isOwner)
local function verify_room_membership(matrix)
local request = http_request.new_from_uri(string.format("%s/verify/user_in_room", uvsUrl));
request.headers:upsert(":method", "POST");
Expand All @@ -100,10 +194,16 @@ local function verify_room_membership(matrix)
if status == "200" then
local data = json.decode(body);
if data.results and data.results.user == true and data.results.room_membership == true then
return true;
if uvsSyncPowerLevels and data.power_levels ~= nil then
-- If the user power in the room is at least "state_detault", we mark them as owner
if data.power_levels.user >= data.power_levels.room.state_default then
return true, true;
end
end
return true, false;
end
end
return false;
return false, false;
end

local function process_and_verify_token(session)
Expand All @@ -124,12 +224,14 @@ local function process_and_verify_token(session)
return false, "access-denied", "Jitsi room does not match Matrix room"
end

local result = verify_room_membership(data.context.matrix)
local isMember, isOwner = verify_room_membership(data.context.matrix)

if result == false then
return false, "access-denied", "Token invalid or not in room";
if isMember == false then
return false, "access-denied", "Token invalid or not in room";
end

-- Store the isOwner detail
session.auth_matrix_user_verification_is_owner = isOwner
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, may be worth adding the room name either as a separate variable or to the name of this one? Otherwise I wonder if it would be possible to auth yourself against one matrix room but then gain moderator status in a different jitsi conference, somehow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not entirely sure. I was thinking of adding the Jitsi room name, but it's already in the session. And using a session for some room against another room seems unlikely to be possible, considering these are internal structures according to the Prosody team?

There is this above: if not (target_room_name == session.jitsi_meet_room) then which if the check failes does not owner the user in the room. I'm not entirely sure why that is there, I took it from the Jitsi Meet plugin that admins everyone (https://github.com/jitsi/jitsi-meet/blob/master/resources/prosody-plugins/mod_muc_allowners.lua#L63), but it's possible it's something to do with ensuring the session is for this particular room. If that is the case, it will also protect in this case I believe.

Adding the room name here and checking it in the later phase could be done but it feels like that is already happening. We don't have the matrix room ID in the "participant joined" hook, so we can't really compare to that, and the jitsi room name is already being compared to the one in the session.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok, if that jitsi room name in the session is defined to be the one matching where the power levels come from then yeah, it should be fine

-- Store some data in the session from the token
session.jitsi_meet_context_user = data.context.user;
session.jitsi_meet_context_features = data.context.features;
Expand Down
Binary file modified widget_load.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.