Skip to content

Commit 3184fb1

Browse files
ThanhDodeurOdooalexkuhn
authored andcommitted
[IMP] remove dependency on jwt library
Instead, it is now a local implementation of JWT, this makes the server less dependent on external libraries for its security and removes a potential source of dependency injection. This commit also removes unnecessary async/await (the old jsonwebtoken was also used synchronously).
1 parent 4fc865e commit 3184fb1

File tree

7 files changed

+319
-156
lines changed

7 files changed

+319
-156
lines changed

package-lock.json

-138
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"#tests/*": "./tests/*"
2424
},
2525
"dependencies": {
26-
"jsonwebtoken": "^9.0.2",
2726
"mediasoup": "~3.15.6",
2827
"ws": "^8.18.1"
2928
},

src/services/auth.js

+145-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,52 @@
1-
import jwt from "jsonwebtoken";
1+
import crypto from "node:crypto";
22

33
import * as config from "#src/config.js";
44
import { Logger } from "#src/utils/utils.js";
55
import { AuthenticationError } from "#src/utils/errors.js";
66

7+
/**
8+
* JsonWebToken header
9+
* https://datatracker.ietf.org/doc/html/rfc7519#section-5
10+
*
11+
* @typedef {Object} JWTHeader
12+
* @property {string} alg - Algorithm used to sign the token
13+
* @property {string} typ - Type of the token, usually "JWT"
14+
*/
15+
16+
/**
17+
* @typeDef {Object} PrivateJWTClaims
18+
* @property {string} sfu_channel_uuid
19+
* @property {number} session_id
20+
* @property {Object[]} ice_servers
21+
*/
22+
23+
/**
24+
* JsonWebToken claims
25+
* https://datatracker.ietf.org/doc/html/rfc7519#section-4
26+
*
27+
* @typedef {PrivateJWTClaims & Object} JWTClaims
28+
* @property {number} [exp] - Expiration time (in seconds since epoch)
29+
* @property {number} [iat] - Issued at (in seconds since epoch)
30+
* @property {number} [nbf] - Not before (in seconds since epoch)
31+
* @property {string} [iss] - Issuer
32+
* @property {string} [sub] - Subject
33+
* @property {string} [aud] - Audience
34+
* @property {string} [jti] - JWT ID
35+
*/
36+
737
let jwtKey;
838
const logger = new Logger("AUTH");
39+
const ALGORITHM = {
40+
HS256: "HS256",
41+
};
42+
const ALGORITHM_FUNCTIONS = {
43+
[ALGORITHM.HS256]: (data, key) => crypto.createHmac("sha256", key).update(data).digest(),
44+
};
945

1046
/**
1147
* @param {WithImplicitCoercion<string>} [key] buffer/b64 str
1248
*/
13-
export async function start(key) {
49+
export function start(key) {
1450
const keyB64str = key || config.AUTH_KEY;
1551
jwtKey = Buffer.from(keyB64str, "base64");
1652
logger.info(`auth key set`);
@@ -20,18 +56,119 @@ export function close() {
2056
jwtKey = undefined;
2157
}
2258

59+
/**
60+
* @param {Buffer|string} data - The data to encode
61+
* @returns {string} - base64 encoded string
62+
*/
63+
export function base64Encode(data) {
64+
if (typeof data === "string") {
65+
data = Buffer.from(data);
66+
}
67+
return data.toString("base64");
68+
}
69+
70+
/**
71+
* @param {string} str base64 encoded string
72+
* @returns {Buffer}
73+
*/
74+
function base64Decode(str) {
75+
let output = str;
76+
const paddingLength = 4 - (output.length % 4);
77+
if (paddingLength < 4) {
78+
output += "=".repeat(paddingLength);
79+
}
80+
return Buffer.from(output, "base64");
81+
}
82+
83+
/**
84+
* Signs and creates a JsonWebToken
85+
*
86+
* @param {JWTClaims} claims - The claims to include in the token
87+
* @param {WithImplicitCoercion<string>} [key] - Optional key, defaults to the configured jwtKey
88+
* @param {Object} [options]
89+
* @param {string} [options.algorithm] - The algorithm to use, defaults to HS256
90+
* @returns {string} - The signed JsonWebToken
91+
* @throws {AuthenticationError}
92+
*/
93+
export function sign(claims, key = jwtKey, { algorithm = ALGORITHM.HS256 } = {}) {
94+
if (!key) {
95+
throw new AuthenticationError("JWT signing key is not set");
96+
}
97+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, "base64");
98+
const headerB64 = base64Encode(JSON.stringify({ alg: algorithm, typ: "JWT" }));
99+
const claimsB64 = base64Encode(JSON.stringify(claims));
100+
const signedData = `${headerB64}.${claimsB64}`;
101+
const signature = ALGORITHM_FUNCTIONS[algorithm]?.(signedData, keyBuffer);
102+
if (!signature) {
103+
throw new AuthenticationError(`Unsupported algorithm: ${algorithm}`);
104+
}
105+
const signatureB64 = base64Encode(signature);
106+
return `${headerB64}.${claimsB64}.${signatureB64}`;
107+
}
108+
109+
/**
110+
* Parses a JsonWebToken into its components
111+
*
112+
* @param {string} token
113+
* @returns {{header: JWTHeader, claims: JWTClaims, signature: Buffer, signedData: string}}
114+
*/
115+
function parseJwt(token) {
116+
const parts = token.split(".");
117+
if (parts.length !== 3) {
118+
throw new AuthenticationError("Invalid JWT format");
119+
}
120+
const [headerB64, claimsB64, signatureB64] = parts;
121+
const header = JSON.parse(base64Decode(headerB64).toString());
122+
const claims = JSON.parse(base64Decode(claimsB64).toString());
123+
const signature = base64Decode(signatureB64);
124+
const signedData = `${headerB64}.${claimsB64}`;
125+
126+
return { header, claims, signature, signedData };
127+
}
128+
129+
function safeEqual(a, b) {
130+
if (a.length !== b.length) {
131+
return false;
132+
}
133+
try {
134+
return crypto.timingSafeEqual(a, b);
135+
} catch {
136+
return false;
137+
}
138+
}
139+
23140
/**
24141
* @param {string} jsonWebToken
25142
* @param {WithImplicitCoercion<string>} [key] buffer/b64 str
26-
* @returns {Promise<any>} json serialized data
143+
* @returns {JWTClaims} claims
27144
* @throws {AuthenticationError}
28145
*/
29-
export async function verify(jsonWebToken, key = jwtKey) {
146+
export function verify(jsonWebToken, key = jwtKey) {
147+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, "base64");
148+
let parsedJWT;
30149
try {
31-
return jwt.verify(jsonWebToken, key, {
32-
algorithms: ["HS256"],
33-
});
150+
parsedJWT = parseJwt(jsonWebToken);
34151
} catch {
35-
throw new AuthenticationError("JsonWebToken verification error");
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");
169+
}
170+
if (claims.iat && claims.iat > now + 60) {
171+
throw new AuthenticationError("Token issued in the future");
36172
}
173+
return claims;
37174
}

src/services/http.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function start({ httpInterface = config.HTTP_INTERFACE, port = conf
5353
try {
5454
const jsonWebToken = req.headers.authorization?.split(" ")[1];
5555
/** @type {{ iss: string, key: string || undefined }} */
56-
const claims = await auth.verify(jsonWebToken);
56+
const claims = auth.verify(jsonWebToken);
5757
if (!claims.iss) {
5858
logger.warn(`${remoteAddress}: missing issuer claim when creating channel`);
5959
res.statusCode = 403; // forbidden
@@ -82,7 +82,7 @@ export async function start({ httpInterface = config.HTTP_INTERFACE, port = conf
8282
try {
8383
const jsonWebToken = await parseBody(req);
8484
/** @type {{ sessionIdsByChannel: Object<string, number[]> }} */
85-
const claims = await auth.verify(jsonWebToken);
85+
const claims = auth.verify(jsonWebToken);
8686
for (const [channelUuid, sessionIds] of Object.entries(
8787
claims.sessionIdsByChannel
8888
)) {

0 commit comments

Comments
 (0)