Skip to content

Commit 932dfb9

Browse files
ThanhDodeurOdooalexkuhn
authored andcommitted
[IMP] add tests for channel auth keys
1 parent 48eea2a commit 932dfb9

File tree

5 files changed

+52
-19
lines changed

5 files changed

+52
-19
lines changed

src/services/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as config from "#src/config.ts";
44
import { Logger } from "#src/utils/utils.ts";
55
import { AuthenticationError } from "#src/utils/errors.ts";
66
import type { SessionId } from "#src/models/session.ts";
7+
import type { StringLike } from "#src/shared/types.ts";
78

89
/**
910
* JsonWebToken (JOSE) header
@@ -124,7 +125,7 @@ function base64Decode(str: string): Buffer {
124125
*/
125126
export function sign(
126127
claims: JWTClaims,
127-
key: Buffer | string = jwtKey!,
128+
key: StringLike = jwtKey!,
128129
options: SignOptions = {}
129130
): string {
130131
const { algorithm = ALGORITHM.HS256 } = options;
@@ -180,7 +181,7 @@ function safeEqual(a: Buffer, b: Buffer): boolean {
180181
/**
181182
* @throws {AuthenticationError} If verification fails
182183
*/
183-
export function verify(jsonWebToken: string, key: Buffer | string = jwtKey!): JWTClaims {
184+
export function verify(jsonWebToken: string, key: StringLike = jwtKey!): JWTClaims {
184185
if (!key) {
185186
throw new AuthenticationError("JWT verification key is not set");
186187
}

src/shared/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type JSONSerializable =
88

99
export type StreamType = "audio" | "camera" | "screen";
1010

11+
export type StringLike = Buffer | string;
12+
1113
import type { DownloadStates } from "#src/client.ts";
1214
import type { SessionId, SessionInfo, TransportConfig } from "#src/models/session.ts";
1315

tests/network.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ describe("Full network", () => {
268268
expect(closeEvent.code).toBe(SESSION_CLOSE_CODE.P_TIMEOUT);
269269
});
270270
test("A client can broadcast arbitrary messages to other clients on a channel that does not have webRTC", async () => {
271-
const channelUUID = await network.getChannelUUID(false);
271+
const channelUUID = await network.getChannelUUID({ useWebRtc: false });
272272
const user1 = await network.connect(channelUUID, 1);
273273
const user2 = await network.connect(channelUUID, 2);
274274
const sender = await network.connect(channelUUID, 3);

tests/security.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,21 @@ describe("Security", () => {
3838
const [event] = await once(websocket, "close");
3939
expect(event).toBe(WS_CLOSE_CODE.TIMEOUT);
4040
});
41+
test("cannot access a channel with the wrong key", async () => {
42+
const channelUUID = await network.getChannelUUID({ key: "channel-specific-key" });
43+
const channel = Channel.records.get(channelUUID);
44+
// testing the default/global key
45+
await expect(network.connect(channelUUID, 3)).rejects.toThrow();
46+
expect(channel!.sessions.size).toBe(0);
47+
// any arbitrary wrong key
48+
await expect(network.connect(channelUUID, 3, { key: "wrong-key" })).rejects.toThrow();
49+
expect(channel!.sessions.size).toBe(0);
50+
});
51+
test("can join a channel with its specific key", async () => {
52+
const key = "channel-specific-key";
53+
const channelUUID = await network.getChannelUUID({ key });
54+
const channel = Channel.records.get(channelUUID);
55+
await network.connect(channelUUID, 4, { key });
56+
expect(channel!.sessions.size).toBe(1);
57+
});
4158
});

tests/utils/network.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SfuClient, SfuClientState } from "#src/client";
1010
import { Channel } from "#src/models/channel";
1111
import type { Session } from "#src/models/session";
1212
import type { JWTClaims } from "#src/services/auth";
13+
import { StringLike } from "#src/shared/types.ts";
1314

1415
/**
1516
* HMAC key for JWT signing in tests
@@ -21,10 +22,11 @@ const HMAC_KEY = Buffer.from(HMAC_B64_KEY, "base64");
2122
* Creates a JWT token for testing
2223
*
2324
* @param data - Claims to include in the JWT
25+
* @param [key] - Key to sign the JWT with
2426
* @returns Signed JWT string
2527
*/
26-
export function makeJwt(data: JWTClaims): string {
27-
return auth.sign(data, HMAC_KEY, { algorithm: auth.ALGORITHM.HS256 });
28+
export function makeJwt(data: JWTClaims, key: StringLike = HMAC_KEY): string {
29+
return auth.sign(data, key, { algorithm: auth.ALGORITHM.HS256 });
2830
}
2931

3032
/**
@@ -51,7 +53,7 @@ export class LocalNetwork {
5153
public port?: number;
5254

5355
/** JWT creation function (can be overridden for testing) */
54-
public makeJwt: (data: JWTClaims) => string = makeJwt;
56+
public makeJwt: (data: JWTClaims, key?: StringLike) => string = makeJwt;
5557

5658
/** Active SFU client instances */
5759
private readonly _sfuClients: SfuClient[] = [];
@@ -74,17 +76,19 @@ export class LocalNetwork {
7476

7577
/**
7678
* Creates a new channel and returns its UUID
77-
*
78-
* @param useWebRtc - Whether to enable WebRTC for the channel
79+
* @param [param0] - options
80+
* @param [param0.useWebRtc=true] - Whether to enable WebRTC for the channel
81+
* @param [param0.key=HMAC_B64_KEY] - Channel key
7982
* @returns Promise resolving to channel UUID
8083
*/
81-
async getChannelUUID(useWebRtc: boolean = true): Promise<string> {
84+
async getChannelUUID({ useWebRtc = true, key = HMAC_B64_KEY } = {}): Promise<string> {
8285
if (!this.hostname || !this.port) {
8386
throw new Error("Network not started - call start() first");
8487
}
8588

8689
const jwt = this.makeJwt({
87-
iss: `http://${this.hostname}:${this.port}/`
90+
iss: `http://${this.hostname}:${this.port}/`,
91+
key
8892
});
8993

9094
const response = await fetch(
@@ -110,10 +114,16 @@ export class LocalNetwork {
110114
*
111115
* @param channelUUID - Channel UUID to connect to
112116
* @param sessionId - Session identifier
117+
* @param [param2]
118+
* @param [param2.key=HMAC_B64_KEY] - Channel key
113119
* @returns Promise resolving to connection result
114120
* @throws {Error} If client is closed before authentication
115121
*/
116-
async connect(channelUUID: string, sessionId: number): Promise<ConnectionResult> {
122+
async connect(
123+
channelUUID: string,
124+
sessionId: number,
125+
{ key = HMAC_KEY }: { key?: StringLike } = {}
126+
): Promise<ConnectionResult> {
117127
if (!this.hostname || !this.port) {
118128
throw new Error("Network not started - call start() first");
119129
}
@@ -122,16 +132,16 @@ export class LocalNetwork {
122132
const sfuClient = new SfuClient();
123133
this._sfuClients.push(sfuClient);
124134

125-
// Override device creation for testing environment
126-
(sfuClient as any)._createDevice = (): Device => {
135+
// @ts-expect-error private property
136+
sfuClient._createDevice = (): Device => {
127137
// Mock device creation since we're in a server environment without real WebRTC
128138
return new Device({
129139
handlerFactory: FakeHandler.createFactory(fakeParameters)
130140
});
131141
};
132142

133-
// Override WebSocket creation for Node.js environment
134-
(sfuClient as any)._createWebSocket = (url: string): WebSocket => {
143+
// @ts-expect-error private property
144+
sfuClient._createWebSocket = (url: string): WebSocket => {
135145
// Replace browser WebSocket with Node.js ws package
136146
return new WebSocket(url);
137147
};
@@ -164,10 +174,13 @@ export class LocalNetwork {
164174
// Start connection
165175
sfuClient.connect(
166176
`ws://${this.hostname}:${this.port}`,
167-
this.makeJwt({
168-
sfu_channel_uuid: channelUUID,
169-
session_id: sessionId
170-
}),
177+
this.makeJwt(
178+
{
179+
sfu_channel_uuid: channelUUID,
180+
session_id: sessionId
181+
},
182+
key
183+
),
171184
{ channelUUID }
172185
);
173186

0 commit comments

Comments
 (0)