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

424 lines
15 KiB
JavaScript

import assert from 'assert';
import http from 'http';
import { mintAsapToken } from './helpers/jwt.js';
import { createXmppClient, joinWithFocus } from './helpers/xmpp_client.js';
/**
* Fetches session-info for the given full JID.
*
* @param {string} jid
* @returns {Promise<object>}
*/
function getSessionInfo(jid) {
return new Promise((resolve, reject) => {
const url = `http://localhost:5280/test-observer/session-info?jid=${encodeURIComponent(jid)}`;
http.get(url, res => {
let body = '';
res.on('data', chunk => {
body += chunk;
});
res.on('end', () => {
if (res.statusCode !== 200) {
reject(new Error(`session-info returned ${res.statusCode}: ${body}`));
return;
}
try {
resolve(JSON.parse(body));
} catch (e) {
reject(new Error(`session-info bad JSON: ${body}`));
}
});
}).on('error', reject);
});
}
/**
* Connects to the main VirtualHost (RS256 / ASAP key-server auth).
* Tokens must be signed with the test RSA private key; Prosody fetches the
* matching public key from mod_test_observer_http's /asap-keys/ route.
*/
function asapClient(params) {
return createXmppClient({ params });
}
describe('mod_auth_token (ASAP / RS256)', () => {
const clients = [];
afterEach(async () => {
await Promise.all(clients.map(c => c.disconnect()));
clients.length = 0;
});
it('connects successfully with a valid RS256 token', async () => {
const token = mintAsapToken({ room: '*' });
const c = await asapClient({ token });
clients.push(c);
assert.ok(c.jid, 'client should have a JID after connecting');
});
it('rejects connection with wrong private key', async () => {
// Sign with a freshly generated key that the server does not know.
const { generateKeyPairSync } = await import('crypto');
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
const wrongPem = privateKey.export({ type: 'pkcs8',
format: 'pem' });
const token = mintAsapToken({}, { privateKey: wrongPem });
await assert.rejects(
() => asapClient({ token }),
/not-allowed/
);
});
it('rejects connection with expired token', async () => {
const token = mintAsapToken({}, { expired: true });
await assert.rejects(
() => asapClient({ token }),
/not-allowed/
);
});
it('rejects connection with not-yet-valid token (nbf in the future)', async () => {
const token = mintAsapToken({}, { notYetValid: true });
await assert.rejects(
() => asapClient({ token }),
/not-allowed/
);
});
it('rejects connection with wrong issuer', async () => {
const token = mintAsapToken({ iss: 'other-app' });
await assert.rejects(
() => asapClient({ token }),
/not-allowed/
);
});
it('sets session.jitsi_meet_context_features from token context', async () => {
const token = mintAsapToken({
room: '*',
context: {
features: {
'screen-sharing': true,
'recording': false
}
}
});
const c = await asapClient({ token });
clients.push(c);
const info = await getSessionInfo(c.jid);
assert.strictEqual(info.jitsi_meet_context_features['screen-sharing'], true);
assert.strictEqual(info.jitsi_meet_context_features.recording, false);
});
it('sets session.jitsi_meet_room from room claim', async () => {
const token = mintAsapToken({ room: 'testroom' });
const c = await asapClient({ token });
clients.push(c);
const info = await getSessionInfo(c.jid);
assert.strictEqual(info.jitsi_meet_room, 'testroom');
});
it('sets session.jitsi_meet_context_user from token context', async () => {
const token = mintAsapToken({
room: '*',
context: {
user: { id: 'user-123',
name: 'Alice',
email: 'alice@example.com' }
}
});
const c = await asapClient({ token });
clients.push(c);
const info = await getSessionInfo(c.jid);
assert.strictEqual(info.jitsi_meet_context_user.id, 'user-123');
assert.strictEqual(info.jitsi_meet_context_user.name, 'Alice');
assert.strictEqual(info.jitsi_meet_context_user.email, 'alice@example.com');
});
it('sets session.jitsi_meet_context_group from token context', async () => {
const token = mintAsapToken({
room: '*',
context: { group: 'test-group' }
});
const c = await asapClient({ token });
clients.push(c);
const info = await getSessionInfo(c.jid);
assert.strictEqual(info.jitsi_meet_context_group, 'test-group');
});
it('sets session.jitsi_meet_context_user.id from top-level user_id when context is absent', async () => {
const token = mintAsapToken({ room: '*',
// eslint-disable-next-line camelcase
user_id: 'legacy-user-456' });
const c = await asapClient({ token });
clients.push(c);
const info = await getSessionInfo(c.jid);
assert.strictEqual(info.jitsi_meet_context_user.id, 'legacy-user-456');
});
it('allows connection without token when allow_empty_token is true', async () => {
const c = await asapClient({});
clients.push(c);
assert.ok(c.jid, 'client should have a JID');
});
// ── Room regex claim: invalid Lua pattern ─────────────────────────────────
//
// When context.room.regex == true, util.lib.lua uses the JWT room claim as
// a Lua pattern via string:match() inside verify_room() at MUC join time.
// An invalid Lua pattern (e.g. unbalanced parenthesis) must be rejected
// cleanly rather than crashing Prosody with a Lua error.
it('does not crash on connect with invalid Lua regex room claim', async () => {
// process_and_verify_token only stores the claim; it does not evaluate
// the pattern. The crash path is verify_room(), called at MUC join.
const token = mintAsapToken({
room: '(invalid',
context: { room: { regex: true } }
});
const c = await asapClient({ token });
clients.push(c);
assert.ok(c.jid, 'connection must succeed: pattern not evaluated at auth time');
});
it('rejects MUC join with invalid Lua regex room claim', async () => {
const CONFERENCE = 'conference.localhost';
const roomJid = `regex-invalid-${Date.now()}@${CONFERENCE}`;
// '(invalid' is an unbalanced Lua pattern capture — string:match() would
// raise "unfinished capture" without the pcall guard in verify_room().
const token = mintAsapToken({
room: '(invalid',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await asapClient({ token });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.equal(presence.attrs.type, 'error', 'join must be rejected');
assert.ok(
presence.getChild('error')?.getChild('invalid-regex'),
'error must contain <invalid-regex/>'
);
});
// ── Room regex claim: pattern is used as written ──────────────────────────
//
// Lua pattern classes are case-sensitive: %d is a digit, %D is anything
// but a digit. verify_room() must therefore not change the case of a room
// claim that is flagged as a pattern.
// Letters only, so that the room name matches %D+ ("not a digit").
const lettersOnly = () => Date.now()
.toString(36)
.replace(/[0-9]/g, 'x');
it('accepts MUC join when the regex room claim uses an uppercase class', async () => {
const CONFERENCE = 'conference.localhost';
const roomJid = `rxcase${lettersOnly()}@${CONFERENCE}`;
const token = mintAsapToken({
room: 'rxcase%D+',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await asapClient({ token });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.notEqual(presence.attrs.type, 'error',
`join must succeed: ${presence.toString()}`);
});
it('rejects MUC join when the room does not match the regex room claim', async () => {
const CONFERENCE = 'conference.localhost';
const roomJid = `rxcase${Date.now()}@${CONFERENCE}`;
// 'rxcase%D+' does not match a room name that ends in digits.
const token = mintAsapToken({
room: 'rxcase%D+',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await asapClient({ token });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.equal(presence.attrs.type, 'error', 'join must be rejected');
assert.ok(
presence.getChild('error')?.getChild('room-name-does-not-match'),
'error must contain <room-name-does-not-match/>'
);
});
// ── Room regex claim: matching limits ─────────────────────────────────────
//
// Lua patterns backtrack, and one match blocks Prosody. verify_room()
// therefore refuses to match a room name that is longer than
// token_regex_room_max_length (128), or a pattern that has more than
// token_regex_max_quantifiers (3) quantifiers. Both refusals report
// <invalid-regex/>, which distinguishes them from a pattern that was
// evaluated and did not match (<room-name-does-not-match/>).
it('accepts MUC join when the regex room claim is at the quantifier limit', async () => {
const CONFERENCE = 'conference.localhost';
const roomJid = `rxlimit${lettersOnly()}@${CONFERENCE}`;
// Three quantifiers is the default limit, and must still be allowed.
const token = mintAsapToken({
room: 'rxlimit%D*%D*%D+',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await asapClient({ token });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.notEqual(presence.attrs.type, 'error',
`join must succeed: ${presence.toString()}`);
});
it('rejects MUC join when the regex room claim has too many quantifiers', async () => {
const CONFERENCE = 'conference.localhost';
// Under the 128-char room-name limit, so the quantifier limit is what
// must reject this join.
const roomJid = `${'a'.repeat(90)}${lettersOnly()}@${CONFERENCE}`;
// The pathological case: four quantifiers over a long run of 'a's, and
// a trailing '%d' that a letters-only room name can never satisfy, so
// every combination is tried before the match fails.
const token = mintAsapToken({
room: 'a*a*a*a*%d',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await asapClient({ token });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.equal(presence.attrs.type, 'error', 'join must be rejected');
assert.ok(
presence.getChild('error')?.getChild('invalid-regex'),
`pattern must be refused unevaluated: ${presence.toString()}`
);
});
it('rejects MUC join when the room name is too long to match a regex claim', async () => {
const CONFERENCE = 'conference.localhost';
// Over the 128-char room-name limit.
const roomJid = `${'x'.repeat(150)}${lettersOnly()}@${CONFERENCE}`;
// '.*' matches any room name, so without the length limit this join
// would be allowed.
const token = mintAsapToken({
room: '.*',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await asapClient({ token });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.equal(presence.attrs.type, 'error', 'join must be rejected');
assert.ok(
presence.getChild('error')?.getChild('invalid-regex'),
`long room name must not be matched: ${presence.toString()}`
);
});
// ── Room regex claim: token_regex_enabled = false ─────────────────────────
//
// conference.noregex.test has a parent host that sets
// token_regex_enabled = false, so a room claim flagged as a pattern is
// refused instead of matched. The same claim is accepted on
// conference.localhost, where the option keeps its default.
it('rejects MUC join with a regex room claim when patterns are disabled', async () => {
const roomName = `rxoff${lettersOnly()}`;
const roomJid = `${roomName}@conference.noregex.test`;
// Matches the room name, so only the disabled option can reject it.
const token = mintAsapToken({
room: 'rxoff%D+',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await createXmppClient({ domain: 'noregex.test',
params: { token } });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.equal(presence.attrs.type, 'error', 'join must be rejected');
assert.ok(
presence.getChild('error')?.getChild('invalid-regex'),
`pattern must be refused: ${presence.toString()}`
);
});
it('accepts the same regex room claim on a host where patterns are enabled', async () => {
const roomName = `rxoff${lettersOnly()}`;
const roomJid = `${roomName}@conference.localhost`;
const token = mintAsapToken({
room: 'rxoff%D+',
context: { room: { regex: true } }
});
const focus = await joinWithFocus(roomJid);
const c = await asapClient({ token });
clients.push(focus, c);
const presence = await c.joinRoom(roomJid);
assert.notEqual(presence.attrs.type, 'error',
`join must succeed: ${presence.toString()}`);
});
});