Files
upstream-jitsi-meet/tests/prosody/mod_presence_identity_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

299 lines
11 KiB
JavaScript

import { xml } from '@xmpp/client';
import assert from 'assert';
import { mintToken } from './helpers/jwt.js';
import { createXmppClient } from './helpers/xmpp_client.js';
const CONFERENCE = 'conference-identity.localhost';
let _roomCounter = 0;
const room = () => `presence-identity-${++_roomCounter}@${CONFERENCE}`;
/**
* Connects to the hs256.localhost VirtualHost with a signed token.
* mod_presence_identity is loaded on this VirtualHost.
*
* @param {string} roomJid full room JID, e.g. 'room@conference-identity.localhost'
* @param {object} [tokenPayload] JWT payload overrides (e.g. { context: { user: ... } }).
* @returns {Promise<XmppTestClient>}
*/
function hs256Client(roomJid, tokenPayload = {}) {
const roomName = roomJid.split('@')[0];
const token = mintToken({ room: roomName,
...tokenPayload });
return createXmppClient({ domain: 'hs256.localhost',
params: { token } });
}
/**
* Returns the first presence stanza in the observer's queue that comes from
* the given room and is NOT the observer's own self-presence.
*
* @param {XmppTestClient} observer
* @param {string} roomJid
* @param {string} observerNick
*/
function waitForOtherPresence(observer, roomJid, observerNick) {
return observer.waitForPresence(
p => p.attrs.from?.startsWith(`${roomJid}/`)
&& p.attrs.from !== `${roomJid}/${observerNick}`
);
}
describe('mod_presence_identity', () => {
const clients = [];
afterEach(async () => {
await Promise.all(clients.map(c => c.disconnect()));
clients.length = 0;
});
// -------------------------------------------------------------------------
// identity injection
// -------------------------------------------------------------------------
describe('identity injection', () => {
it('injects <identity><user> from JWT context.user into MUC presence', async () => {
const r = room();
const identity = await hs256Client(r, {
context: { user: { id: 'u1',
name: 'Alice' } }
});
clients.push(identity);
await identity.joinRoom(r);
const observer = await createXmppClient();
clients.push(observer);
await observer.joinRoom(r);
const presence = await waitForOtherPresence(observer, r, observer.nick);
const identityEl = presence.getChild('identity');
assert.ok(identityEl, 'presence must contain <identity>');
const userEl = identityEl.getChild('user');
assert.ok(userEl, '<identity> must contain <user>');
assert.strictEqual(userEl.getChild('id')?.text(), 'u1', 'id must match token');
assert.strictEqual(userEl.getChild('name')?.text(), 'Alice', 'name must match token');
});
it('adds <group> inside <identity> when context.group is set', async () => {
const r = room();
const identity = await hs256Client(r, {
context: {
user: { id: 'u2' },
group: 'mygroup'
}
});
clients.push(identity);
await identity.joinRoom(r);
const observer = await createXmppClient();
clients.push(observer);
await observer.joinRoom(r);
const presence = await waitForOtherPresence(observer, r, observer.nick);
const identityEl = presence.getChild('identity');
assert.ok(identityEl, 'presence must contain <identity>');
assert.strictEqual(identityEl.getChild('group')?.text(), 'mygroup',
'<group> must match token context.group');
});
it('does not inject <identity> when token has no context.user', async () => {
const r = room();
// Valid token but no context field → no jitsi_meet_context_user on session.
const noContext = await hs256Client(r);
clients.push(noContext);
await noContext.joinRoom(r);
const observer = await createXmppClient();
clients.push(observer);
await observer.joinRoom(r);
const presence = await waitForOtherPresence(observer, r, observer.nick);
assert.ok(!presence.getChild('identity'),
'presence must not contain <identity> when token has no context.user');
});
it('injects <identity> in presence updates (not only on join)', async () => {
const r = room();
const observer = await createXmppClient();
clients.push(observer);
await observer.joinRoom(r);
const identity = await hs256Client(r, {
context: { user: { id: 'u3',
name: 'Bob' } }
});
clients.push(identity);
await identity.joinRoom(r);
// Consume the join presence.
const joinPresence = await waitForOtherPresence(observer, r, observer.nick);
assert.ok(joinPresence.getChild('identity'), 'join presence must have <identity>');
// Send a presence update (no <x muc> element — this is a regular update,
// not a join). Both pre-presence/bare and pre-presence/full hooks fire.
const identityNick = joinPresence.attrs.from.split('/')[1];
await identity.sendPresence(`${r}/${identityNick}`);
// The update must also carry <identity>.
const updatePresence = await observer.waitForPresence(
p => p.attrs.from === `${r}/${identityNick}`
);
assert.ok(updatePresence.getChild('identity'),
'presence update must also carry <identity>');
});
});
// -------------------------------------------------------------------------
// malformed JWT context.user keys
// -------------------------------------------------------------------------
//
// update_presence_identity() iterates over JWT context.user keys and uses
// each key as an XML element name: presence:tag(key):text(tostring(value)).
// Fixed in 2e8980d2b: keys that do not match the XML name pattern
// ^[%a_][%w%-%.%:_]*$ are now silently skipped before reaching tag().
// This test verifies that bad keys are dropped and valid keys are preserved.
describe('malformed JWT context.user keys', () => {
it('survives context.user keys that contain XML-special characters', async () => {
const r = room();
// Key names with '<' and '>' — these become XML element names.
// Prosody / LuaXML may escape, drop, or crash on them.
const identity = await hs256Client(r, {
context: {
user: {
id: 'safe-id',
'bad<key>': 'value'
}
}
});
clients.push(identity);
await identity.joinRoom(r);
const observer = await createXmppClient();
clients.push(observer);
await observer.joinRoom(r);
// If Prosody crashed the hook the join presence would never arrive.
const presence = await waitForOtherPresence(observer, r, observer.nick);
assert.ok(presence, 'join presence must arrive even with unusual key names');
// The safe 'id' element must be present and correct.
const identityEl = presence.getChild('identity');
assert.ok(identityEl, 'presence must still contain <identity>');
assert.strictEqual(
identityEl.getChild('user')?.getChild('id')
?.text(),
'safe-id',
'safe id field must be present and unmodified'
);
});
});
// -------------------------------------------------------------------------
// spoofing prevention
// -------------------------------------------------------------------------
describe('spoofing prevention', () => {
it('strips a client-supplied <identity> when session has no context.user', async () => {
const r = room();
// Token with no context → no jitsi_meet_context_user on session.
// The client manually crafts an <identity> element in the join presence.
const intruder = await hs256Client(r);
clients.push(intruder);
await intruder.joinRoom(r, undefined, {
extensions: [
xml('identity', {},
xml('user', {},
xml('id', {}, 'hacker'),
xml('name', {}, 'Evil Corp')
)
)
]
});
const observer = await createXmppClient();
clients.push(observer);
await observer.joinRoom(r);
const presence = await waitForOtherPresence(observer, r, observer.nick);
assert.ok(!presence.getChild('identity'),
'module must strip the hand-crafted <identity> when no context.user is set');
});
it('strips a client-supplied <identity> and replaces it with the real one', async () => {
const r = room();
// Token with real context.user but also a crafted <identity> in the stanza.
// The module must strip the crafted one and inject the real one from the session.
const identity = await hs256Client(r, {
context: { user: { id: 'real-id',
name: 'Real User' } }
});
clients.push(identity);
await identity.joinRoom(r, undefined, {
extensions: [
xml('identity', {},
xml('user', {},
xml('id', {}, 'spoofed-id'),
xml('name', {}, 'Spoofed Name')
)
)
]
});
const observer = await createXmppClient();
clients.push(observer);
await observer.joinRoom(r);
const presence = await waitForOtherPresence(observer, r, observer.nick);
const identityEl = presence.getChild('identity');
assert.ok(identityEl, 'presence must contain <identity>');
assert.strictEqual(identityEl.getChild('user')?.getChild('id')
?.text(), 'real-id',
'id must be from the verified JWT context, not the spoofed stanza');
assert.strictEqual(identityEl.getChild('user')?.getChild('name')
?.text(), 'Real User',
'name must be from the verified JWT context, not the spoofed stanza');
});
});
});