Files
upstream-jitsi-meet/tests/prosody/mod_token_verification_spec.js
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

244 lines
9.9 KiB
JavaScript

import assert from 'assert';
import { mintAsapToken } from './helpers/jwt.js';
import { createXmppClient, joinWithFocus } from './helpers/xmpp_client.js';
import { isAvailablePresence } from './helpers/xmpp_utils.js';
// ─── Constants ───────────────────────────────────────────────────────────────
// All token_verification tests run on the main MUC component.
// token_verification_require_token_for_moderation = true is set on
// conference.localhost in the test config; focus@auth.localhost is a Prosody
// admin and is therefore always exempt from both the join check and the
// moderation IQ check, mirroring production.
const CONFERENCE = 'conference.localhost';
let _roomCounter = 0;
const nextRoom = () => `token-verify-${++_roomCounter}@${CONFERENCE}`;
// ─── Tests ───────────────────────────────────────────────────────────────────
describe('mod_token_verification', () => {
const clients = [];
afterEach(async () => {
await Promise.all(clients.map(c => c.disconnect()));
clients.length = 0;
});
// ── Join access control ──────────────────────────────────────────────────
//
// token_verification is loaded on conference.localhost. The parent
// VirtualHost (localhost) has allow_empty_token = true, so anonymous users
// are always let through (verify_room returns true when auth_token is nil).
//
// focus@auth.localhost is a Prosody admin and is therefore exempt from
// the token check on both muc-room-pre-create and muc-occupant-pre-join.
describe('join access control', () => {
/**
* Creates a room with a focus participant for testing.
*
* @returns {Promise<string>} Room JID.
*/
async function setupRoom() {
const room = nextRoom();
const focus = await joinWithFocus(room);
clients.push(focus);
return room;
}
it('allows anonymous join when allow_empty_token = true', async () => {
const room = await setupRoom();
const c = await createXmppClient();
clients.push(c);
const presence = await c.joinRoom(room);
assert.ok(isAvailablePresence(presence),
'tokenless user must be allowed when allow_empty_token = true');
});
it('rejects connection when token is missing the sub claim', async () => {
// sub: undefined overrides the mintAsapToken default of sub: '*', producing
// a token with no sub field. process_and_verify_token rejects such tokens,
// causing a SASL failure before the client finishes connecting.
const token = mintAsapToken({ sub: undefined });
await assert.rejects(
createXmppClient({ params: { token } }),
'connection must be rejected when the sub claim is absent'
);
});
it('rejects connection when token has no room claim', async () => {
// asap_require_room_claim = true: a token without a room claim is
// rejected at SASL / connection time, before the client can join a room.
const token = mintAsapToken({ room: undefined });
await assert.rejects(
createXmppClient({ params: { token } }),
'connection must be rejected when the room claim is absent'
);
});
it('allows join with token whose room claim matches the room', async () => {
const room = await setupRoom();
const roomName = room.split('@')[0]; // e.g. "token-verify-3"
const token = mintAsapToken({ room: roomName });
const c = await createXmppClient({ params: { token } });
clients.push(c);
const presence = await c.joinRoom(room);
assert.ok(isAvailablePresence(presence),
'token with matching room claim must be allowed');
});
it('blocks join when token room claim does not match the room', async () => {
const room = await setupRoom();
// Token claims access to 'other-room', but we try to join a different room.
const token = mintAsapToken({ room: 'other-room' });
const c = await createXmppClient({ params: { token } });
clients.push(c);
const presence = await c.joinRoom(room);
assert.strictEqual(presence.attrs.type, 'error',
'mismatched room claim must be rejected');
assert.ok(
presence.getChild('error')?.getChild('not-allowed'),
'expected <not-allowed/> error condition');
});
it('blocks join for moderator token scoped to a different room', async () => {
const room = await setupRoom();
// Moderator token scoped to 'other-room' — mod_token_verification
// checks the room claim at priority 99, before mod_token_affiliation
// (priority 0) can grant owner status, so being a moderator is no bypass.
const token = mintAsapToken({ room: 'other-room',
context: { user: { moderator: true } } });
const c = await createXmppClient({ params: { token } });
clients.push(c);
const presence = await c.joinRoom(room);
assert.strictEqual(presence.attrs.type, 'error',
'moderator token for wrong room must be rejected');
assert.ok(
presence.getChild('error')?.getChild('not-allowed'),
'expected <not-allowed/> error condition');
});
it('blocks join for member token scoped to a different room', async () => {
const room = await setupRoom();
const token = mintAsapToken({ room: 'other-room',
context: { user: { id: 'user1' } } });
const c = await createXmppClient({ params: { token } });
clients.push(c);
const presence = await c.joinRoom(room);
assert.strictEqual(presence.attrs.type, 'error',
'member token for wrong room must be rejected');
assert.ok(
presence.getChild('error')?.getChild('not-allowed'),
'expected <not-allowed/> error condition');
});
it('allows join with wildcard (*) room claim', async () => {
const room = await setupRoom();
const token = mintAsapToken({ room: '*' });
const c = await createXmppClient({ params: { token } });
clients.push(c);
const presence = await c.joinRoom(room);
assert.ok(isAvailablePresence(presence),
'wildcard room claim must be allowed in any room');
});
});
// ── token_verification_require_token_for_moderation ──────────────────────
//
// conference.localhost has require_token_for_moderation = true.
// Unauthenticated users (no token) are blocked from sending muc#owner IQs,
// which is how moderator status is granted to other participants and how
// room configuration (e.g. passwords) is changed. Authenticated users
// whose token room claim matches are allowed through.
//
// focus@auth.localhost is a Prosody admin and is always exempt from the
// check, so it can create rooms and set their initial configuration.
describe('token_verification_require_token_for_moderation', () => {
/**
* Creates a room with a focus participant for testing.
*
* @returns {Promise<string>} Room JID.
*/
async function setupRoom() {
const room = nextRoom();
const focus = await joinWithFocus(room);
clients.push(focus);
return room;
}
it('blocks unauthenticated user from sending owner config IQ', async () => {
const room = await setupRoom();
// Anonymous guest joins (allow_empty_token = true on localhost).
const guest = await createXmppClient();
clients.push(guest);
await guest.joinRoom(room);
// Guest attempts to change room config (muc#owner IQ).
const iq = await guest.setRoomPassword(room, 'hacked');
assert.strictEqual(iq.attrs.type, 'error',
'unauthenticated user must be blocked from changing room config');
assert.ok(
iq.getChild('error')?.getChild('not-allowed'),
'expected <not-allowed/> error condition from require_token_for_moderation');
});
it('does not block authenticated user from sending owner config IQ', async () => {
const room = await setupRoom();
const roomName = room.split('@')[0];
const token = mintAsapToken({ room: roomName });
const authUser = await createXmppClient({ params: { token } });
clients.push(authUser);
await authUser.joinRoom(room);
// Authenticated user sends an muc#owner IQ. require_token_for_moderation
// must NOT block it (though Prosody may still deny with 'forbidden' if the
// user is not the room owner — that is a different check).
const iq = await authUser.setRoomPassword(room, 'secret');
// The error must not be 'not-allowed' from require_token_for_moderation.
const error = iq.getChild('error');
assert.ok(
!error?.getChild('not-allowed'),
'require_token_for_moderation must not block authenticated user');
});
});
});