Skip to content

Commit 2aab6d6

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 2aab6d6

File tree

2 files changed

+59
-41
lines changed

2 files changed

+59
-41
lines changed

src/services/auth.js

+52-25
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

+7-16
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ 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
14-
* @property {string} channelUUID
14+
* @property {string} channelUUID deprecated, this is obtained from the jwt
1515
* @property {string} jwt
1616
*/
1717

@@ -53,7 +53,6 @@ export async function start(options) {
5353
/** @type {Credentials | String} can be a string (the jwt) for backwards compatibility with version 1.1 and earlier */
5454
const credentials = JSON.parse(message);
5555
const session = connect(webSocket, {
56-
channelUUID: credentials?.channelUUID,
5756
jwt: credentials.jwt || credentials,
5857
});
5958
session.remote = remoteAddress;
@@ -102,22 +101,14 @@ export function close() {
102101
* @param {import("ws").WebSocket} webSocket
103102
* @param {Credentials}
104103
*/
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-
}
104+
function connect(webSocket, { jwt }) {
105+
const token = new JsonWebToken(jwt);
106+
const channel = Channel.records.get(token.unsafe.claims.sfu_channel_uuid);
118107
if (!channel) {
119108
throw new AuthenticationError(`Channel does not exist`);
120109
}
110+
const authResult = token.verify(channel.key);
111+
const { session_id, ice_servers } = authResult;
121112
if (!session_id) {
122113
throw new AuthenticationError("Malformed JWT payload");
123114
}

0 commit comments

Comments
 (0)