1
0

security: bind TURN grants to screen sessions

This commit is contained in:
2026-08-11 21:48:11 -07:00
parent 5ca214e4bb
commit 19f022f71a
16 changed files with 530 additions and 93 deletions
+14
View File
@@ -5,6 +5,14 @@ import {
IceConfigurationUnavailableError,
} from "../media/iceClient.ts";
const authorization = {
binding: { officeId: "lumbridge-hq", levelId: "level-1", roomId: "commons", screenId: "wall-display" },
credential: {
sessionId: "share-session", participantId: "viewer-opaque", role: "viewer" as const,
grantToken: "private-viewer-grant",
},
};
describe("ephemeral ICE configuration client", () => {
it("posts the exact authenticated request and returns an in-memory RTC configuration", async () => {
let input: RequestInfo | URL | undefined;
@@ -32,6 +40,7 @@ describe("ephemeral ICE configuration client", () => {
},
now: () => 200,
requestId: () => "ice-test-1",
...authorization,
});
assert.equal(input, "/api/v1/media/ice");
assert.equal(init?.method, "POST");
@@ -41,6 +50,7 @@ describe("ephemeral ICE configuration client", () => {
type: "ice-config-request",
protocolVersion: 1,
requestId: "ice-test-1",
...authorization,
});
assert.deepEqual(result.configuration.iceServers, [
{ urls: ["stun:relay.example.test:3478"] },
@@ -59,6 +69,7 @@ describe("ephemeral ICE configuration client", () => {
authenticatedFetch: async () => Response.json(body),
now: () => 200,
requestId: () => "ice-test-1",
...authorization,
});
await assert.rejects(run({
type: "ice-config-unavailable",
@@ -98,6 +109,7 @@ describe("ephemeral ICE configuration client", () => {
retryAfterMs: 12_345,
}, { status }),
requestId: () => "ice-backoff-1",
...authorization,
}), (error: unknown) => {
assert.ok(error instanceof IceConfigurationUnavailableError);
assert.equal(error.retryAfterMs, 12_345);
@@ -114,11 +126,13 @@ describe("ephemeral ICE configuration client", () => {
retryAfterMs: 12_345,
}, { status: 503 }),
requestId: () => "ice-backoff-1",
...authorization,
}), /did not match/);
await assert.rejects(fetchEphemeralIceConfiguration({
authenticatedFetch: async () => Response.json({ retryAfterMs: 1 }, { status: 503 }),
requestId: () => "ice-backoff-1",
...authorization,
}), /failed \(503\)/);
});
});
+15 -6
View File
@@ -8,6 +8,16 @@ import {
type IceConfigGrant,
} from "../media/index.ts";
const request = () => ({
type: "ice-config-request",
protocolVersion: 1,
requestId: "ice-request-1",
binding: { officeId: "lumbridge-hq", levelId: "level-1", roomId: "lobby", screenId: "lobby-monitor" },
credential: {
sessionId: "opaque-session", participantId: "opaque-participant", role: "presenter", grantToken: "opaque-grant-token-value",
},
});
const grant = (): IceConfigGrant => ({
type: "ice-config-grant",
protocolVersion: 1,
@@ -27,9 +37,7 @@ const grant = (): IceConfigGrant => ({
describe("ICE configuration wire contract", () => {
it("accepts exact requests and bounded ephemeral grants", () => {
assert.equal(parseIceConfigRequest({
type: "ice-config-request", protocolVersion: 1, requestId: "ice-request-1",
}).ok, true);
assert.equal(parseIceConfigRequest(request()).ok, true);
assert.equal(parseIceConfigResponse(JSON.parse(JSON.stringify(grant()))).ok, true);
assert.equal(isIceConfigGrantActive(grant(), 300_000), true);
assert.equal(isIceConfigGrantActive(grant(), 601_000), false);
@@ -53,9 +61,10 @@ describe("ICE configuration wire contract", () => {
});
it("rejects extra keys, credential tricks, missing TURN, and excessive lifetime", () => {
assert.equal(parseIceConfigRequest({
type: "ice-config-request", protocolVersion: 1, requestId: "ice-request-1", subject: "stable-user",
}).ok, false);
assert.equal(parseIceConfigRequest({ ...request(), subject: "stable-user" }).ok, false);
const { credential: _credential, ...withoutCredential } = request();
assert.equal(parseIceConfigRequest(withoutCredential).ok, false);
assert.equal(parseIceConfigRequest({ ...request(), binding: { ...request().binding, screenId: "bad id" } }).ok, false);
assert.equal(parseIceConfigResponse({ ...grant(), identity: { profile: "karti" } }).ok, false);
assert.equal(parseIceConfigResponse({ ...grant(), iceServers: [{ urls: ["stun:relay.example.test:3478"] }] }).ok, false);
assert.equal(parseIceConfigResponse({ ...grant(), expiresAtMs: 3_601_001 }).ok, false);
+99 -1
View File
@@ -124,6 +124,28 @@ function grant(
};
}
function iceGrant(requestId: string) {
return {
type: "ice-config-grant",
protocolVersion: 1,
requestId,
issuedAtMs: NOW - 1_000,
expiresAtMs: NOW + 50_000,
iceServers: [{
urls: ["turns:relay.example.test:5349"],
username: "temporary-user-01",
credential: "temporary-password-01",
credentialType: "password",
}],
};
}
function iceResponse(input: RequestInfo | URL, body: Record<string, unknown>): Response | null {
return String(input).endsWith("/ice")
? Response.json(iceGrant(String(body.requestId)))
: null;
}
function sse(messages: readonly unknown[]): Response {
const bytes = new TextEncoder().encode(messages.map((message) => `data: ${JSON.stringify(message)}\n\n`).join(""));
return new Response(new ReadableStream<Uint8Array>({
@@ -165,6 +187,8 @@ describe("remote office media presenter", () => {
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
calls.push({ url: String(input), init, body });
const ice = iceResponse(input, body);
if (ice) return ice;
if (String(input).endsWith("/join")) {
return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", []), { status: 201 });
}
@@ -189,6 +213,16 @@ describe("remote office media presenter", () => {
assert.equal(video.autoplay, false);
assert.equal(video.muted, true);
const signal = calls.find((call) => call.url.endsWith("/signal"));
const joinIndex = calls.findIndex((call) => call.url.endsWith("/join"));
const iceIndex = calls.findIndex((call) => call.url.endsWith("/ice"));
assert.ok(joinIndex >= 0 && iceIndex > joinIndex, "signaling authorization precedes TURN exchange");
assert.deepEqual(calls[iceIndex]?.body, {
type: "ice-config-request",
protocolVersion: 1,
requestId: calls[iceIndex]?.body.requestId,
binding: BINDING,
credential: access("presenter", "presenter-opaque").credential,
});
assert.equal((signal?.body.signal as { kind?: string }).kind, "sdp");
assert.equal(signal?.init.credentials, "same-origin");
assert.equal(calls.some((call) => call.url.includes("private-presenter-grant")), false);
@@ -201,11 +235,23 @@ describe("remote office media presenter", () => {
it("tears down failed peers and rebuilds them from a resumed participant snapshot", async () => {
const timers = new FakeTimers();
const peers: FakePeer[] = [];
const iceTokens: string[] = [];
let eventCalls = 0;
const participant = { participantId: "viewer-opaque", role: "viewer" as const };
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
const ice = iceResponse(input, body);
if (ice) {
iceTokens.push((body.credential as { grantToken: string }).grantToken);
return ice;
}
if (String(input).endsWith("/join")) return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", [participant]), { status: 201 });
if (String(input).endsWith("/events")) return sse([grant("screen-share-resume-grant", String(body.requestId), "presenter", [participant])]);
if (String(input).endsWith("/events")) {
eventCalls += 1;
const resumed = grant("screen-share-resume-grant", String(body.requestId), "presenter", [participant]);
resumed.grant.credential.grantToken = `rotated-reconnect-${eventCalls}`;
return sse([resumed]);
}
return new Response(null, { status: 204 });
};
const media = createRemoteOfficeMedia({
@@ -214,6 +260,8 @@ describe("remote office media presenter", () => {
});
const track = new FakeTrack();
await media.startPresenter({ consent: { authorized: true, optedIn: true }, stream: new FakeStream([track]) as unknown as MediaStream, video: new FakeVideo() as unknown as HTMLVideoElement });
await settle();
assert.deepEqual(iceTokens, ["private-presenter-grant"], "connected peer keeps its active TURN allocation across token rotation");
peers[0]?.fail();
assert.equal(media.state().status, "reconnecting");
assert.equal(peers[0]?.closed, 1);
@@ -221,6 +269,8 @@ describe("remote office media presenter", () => {
await settle();
assert.equal(peers.length, 2);
assert.equal(peers[1]?.added[0], track as unknown as MediaStreamTrack);
assert.deepEqual(iceTokens, ["private-presenter-grant", "rotated-reconnect-2"],
"rebuilt peer refreshes TURN under the current rotated grant");
await media.dispose();
assert.equal(track.stopped, 0);
});
@@ -230,6 +280,8 @@ describe("remote office media presenter", () => {
let joinRequestId = "";
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
const ice = iceResponse(input, body);
if (ice) return ice;
if (String(input).endsWith("/join")) {
joinRequestId = String(body.requestId);
return new Promise<Response>((resolve) => { resolveJoin = resolve; });
@@ -257,9 +309,15 @@ describe("remote office media presenter", () => {
it("renews its lease before expiry and uses only the rotated in-memory grant", async () => {
const timers = new FakeTimers();
const eventTokens: string[] = [];
const iceTokens: string[] = [];
let eventCalls = 0;
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
const ice = iceResponse(input, body);
if (ice) {
iceTokens.push((body.credential as { grantToken: string }).grantToken);
return ice;
}
if (String(input).endsWith("/join")) {
return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", []), { status: 201 });
}
@@ -284,13 +342,49 @@ describe("remote office media presenter", () => {
});
await settle();
assert.deepEqual(eventTokens, ["private-presenter-grant"]);
assert.deepEqual(iceTokens, ["private-presenter-grant"], "initial resume reuses active TURN configuration");
assert.equal(timers.pending.size, 1, "one pre-expiry renewal is scheduled");
timers.run();
await settle();
assert.deepEqual(eventTokens, ["private-presenter-grant", "rotated-1"]);
assert.deepEqual(iceTokens, ["private-presenter-grant"], "lease renewal does not issue unused TURN credentials");
assert.equal(JSON.stringify(media.state()).includes("rotated-2"), false);
await media.dispose();
});
it("creates no peer before grant-bound ICE succeeds and cleans up a failed hosted join", async () => {
let peerCount = 0;
const urls: string[] = [];
const fetcher: typeof fetch = async (input, init = {}) => {
const url = String(input);
urls.push(url);
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
if (url.endsWith("/join")) {
return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", [
{ participantId: "viewer-opaque", role: "viewer" },
]), { status: 201 });
}
if (url.endsWith("/ice")) return Response.json({
type: "ice-config-unavailable", protocolVersion: 1,
requestId: body.requestId, retryAfterMs: 10_000,
}, { status: 503 });
return new Response(null, { status: 204 });
};
const track = new FakeTrack();
const media = createRemoteOfficeMedia({
role: "presenter", binding: BINDING, fetch: fetcher, now: () => NOW,
peerConnectionFactory: () => { peerCount += 1; return new FakePeer() as unknown as RTCPeerConnection; },
});
await assert.rejects(media.startPresenter({
consent: { authorized: true, optedIn: true },
stream: new FakeStream([track]) as unknown as MediaStream,
video: new FakeVideo() as unknown as HTMLVideoElement,
}), /temporarily unavailable/);
assert.equal(peerCount, 0);
assert.deepEqual(urls.map((url) => url.split("/").at(-1)), ["join", "ice", "leave"]);
assert.equal(track.stopped, 0);
assert.equal(media.state().status, "idle");
});
});
describe("remote office media viewer", () => {
@@ -301,6 +395,8 @@ describe("remote office media viewer", () => {
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
calls.push({ url: String(input), body });
const iceConfig = iceResponse(input, body);
if (iceConfig) return iceConfig;
if (String(input).endsWith("/join")) return Response.json(grant("screen-share-join-grant", String(body.requestId), "viewer", [presenterPeer]), { status: 201 });
if (String(input).endsWith("/events")) {
const resume = grant("screen-share-resume-grant", String(body.requestId), "viewer", [presenterPeer]);
@@ -348,6 +444,8 @@ describe("remote office media viewer", () => {
let leaveCalls = 0;
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
const ice = iceResponse(input, body);
if (ice) return ice;
if (String(input).endsWith("/join")) {
return Response.json(grant("screen-share-join-grant", String(body.requestId), "viewer", [presenterPeer]), { status: 201 });
}