Skip to content

Commit 40e2cb6

Browse files
[IMP] peek into the JWT to get the channel uuid
Before this commit, the payload of the first websocket message (auth) would expect the channel uuid along the jwt, to know where to look to get the key used to sign it. It was slightly redundant as the channel uuid is part of the jwt payload. With the new JWT implementation, we now have the freedom to read the JWT before verifying it. It also reduces the business code complexity as we no longer need to check the corner case of passing a keyed channel uuid in the jwt while skipping the channelUUID of the websocket payload (see removed code in `connect()` of `ws.js`. This is safe to do as the payload is verified with the key of the channel, which means that the signature has to match the channel uuid and tampering with it would invalidate the content.
1 parent 3184fb1 commit 40e2cb6

File tree

2 files changed

+58
-39
lines changed

2 files changed

+58
-39
lines changed

src/services/auth.js

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -144,31 +144,58 @@ function safeEqual(a, b) {
144144
* @throws {AuthenticationError}
145145
*/
146146
export function verify(jsonWebToken, key = jwtKey) {
147-
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, "base64");
148-
let parsedJWT;
149-
try {
150-
parsedJWT = parseJwt(jsonWebToken);
151-
} catch {
152-
throw new AuthenticationError("Invalid JWT format");
153-
}
154-
const { header, claims, signature, signedData } = parsedJWT;
155-
const expectedSignature = ALGORITHM_FUNCTIONS[header.alg]?.(signedData, keyBuffer);
156-
if (!expectedSignature) {
157-
throw new AuthenticationError(`Unsupported algorithm: ${header.alg}`);
158-
}
159-
if (!safeEqual(signature, expectedSignature)) {
160-
throw new AuthenticationError("Invalid signature");
161-
}
162-
// `exp`, `iat` and `nbf` are in seconds (`NumericDate` per RFC7519)
163-
const now = Math.floor(Date.now() / 1000);
164-
if (claims.exp && claims.exp < now) {
165-
throw new AuthenticationError("Token expired");
166-
}
167-
if (claims.nbf && claims.nbf > now) {
168-
throw new AuthenticationError("Token not valid yet");
147+
const jwt = new JsonWebToken(jsonWebToken);
148+
return jwt.verify(key);
149+
}
150+
151+
export class JsonWebToken {
152+
/**
153+
* @type {{
154+
* header: JWTHeader,
155+
* claims: JWTClaims,
156+
* signature: Buffer,
157+
* signedData: string,
158+
* }}
159+
*/
160+
unsafe;
161+
/**
162+
* @param {string} jsonWebToken
163+
*/
164+
constructor(jsonWebToken) {
165+
let payload;
166+
try {
167+
payload = parseJwt(jsonWebToken);
168+
} catch {
169+
throw new AuthenticationError("Malformed JWT");
170+
}
171+
this.unsafe = payload;
169172
}
170-
if (claims.iat && claims.iat > now + 60) {
171-
throw new AuthenticationError("Token issued in the future");
173+
174+
/**
175+
* @param {WithImplicitCoercion<string>} [key] buffer/b64 str
176+
* @return {JWTClaims}
177+
*/
178+
verify(key = jwtKey) {
179+
const { header, claims, signature, signedData } = this.unsafe;
180+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, "base64");
181+
const expectedSignature = ALGORITHM_FUNCTIONS[header.alg]?.(signedData, keyBuffer);
182+
if (!expectedSignature) {
183+
throw new AuthenticationError(`Unsupported algorithm: ${header.alg}`);
184+
}
185+
if (!safeEqual(signature, expectedSignature)) {
186+
throw new AuthenticationError("Invalid signature");
187+
}
188+
// `exp`, `iat` and `nbf` are in seconds (`NumericDate` per RFC7519)
189+
const now = Math.floor(Date.now() / 1000);
190+
if (claims.exp && claims.exp < now) {
191+
throw new AuthenticationError("Token expired");
192+
}
193+
if (claims.nbf && claims.nbf > now) {
194+
throw new AuthenticationError("Token not valid yet");
195+
}
196+
if (claims.iat && claims.iat > now + 60) {
197+
throw new AuthenticationError("Token issued in the future");
198+
}
199+
return claims;
172200
}
173-
return claims;
174201
}

src/services/ws.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Logger, extractRequestInfo } from "#src/utils/utils.js";
77
import { AuthenticationError, OvercrowdedError } from "#src/utils/errors.js";
88
import { SESSION_CLOSE_CODE } from "#src/models/session.js";
99
import { Channel } from "#src/models/channel.js";
10-
import { verify } from "#src/services/auth.js";
10+
import { JsonWebToken } from "#src/services/auth.js";
1111

1212
/**
1313
* @typedef Credentials
@@ -102,19 +102,11 @@ export function close() {
102102
* @param {import("ws").WebSocket} webSocket
103103
* @param {Credentials}
104104
*/
105-
function connect(webSocket, { channelUUID, jwt }) {
106-
let channel = Channel.records.get(channelUUID);
107-
const authResult = verify(jwt, channel?.key);
108-
const { sfu_channel_uuid, session_id, ice_servers } = authResult;
109-
if (!channelUUID && sfu_channel_uuid) {
110-
// Cases where the channelUUID is not provided in the credentials for backwards compatibility with version 1.1 and earlier.
111-
channel = Channel.records.get(sfu_channel_uuid);
112-
if (channel.key) {
113-
throw new AuthenticationError(
114-
"A channel with a key can only be accessed by providing a channelUUID in the credentials"
115-
);
116-
}
117-
}
105+
function connect(webSocket, { jwt }) {
106+
const token = new JsonWebToken(jwt);
107+
const channel = Channel.records.get(token.unsafe.claims.sfu_channel_uuid);
108+
const authResult = token.verify(channel?.key);
109+
const { session_id, ice_servers } = authResult;
118110
if (!channel) {
119111
throw new AuthenticationError(`Channel does not exist`);
120112
}

0 commit comments

Comments
 (0)