Fix JWT security issues in signaling and alert servers (FRE-4497)

- Replace custom JWT parser with jsonwebtoken library (timing-safe HMAC)
- Prefer Authorization header over URL query for token extraction
- Add jsonwebtoken + @types/jsonwebtoken to server dependencies

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-01 09:04:28 -04:00
parent ec4565f44c
commit 3192d1a779
4 changed files with 192 additions and 50 deletions

View File

@@ -2,6 +2,7 @@ import { WebSocketServer, WebSocket, Data } from 'ws';
import { randomBytes } from 'crypto';
import { IncomingMessage } from 'http';
import { EventEmitter } from 'events';
import jwt from 'jsonwebtoken';
/**
* WebSocket Alert Server for Real-Time Call Analysis
@@ -77,32 +78,23 @@ const DEFAULT_CONFIG: AlertServerConfig = {
shutdownTimeoutMs: 5000,
};
// ── JWT Helper (shared with signaling server) ────────────────────────────────
// ── JWT Helper ───────────────────────────────────────────────────────────────
function extractJwtFromQuery(url: string): string | null {
const match = url.match(/[?&]token=([^&]+)/);
function extractJwt(req: IncomingMessage): string | null {
const auth = req.headers['authorization'];
if (auth?.startsWith('Bearer ')) return auth.slice(7);
const match = req.url?.match(/[?&]token=([^&]+)/);
return match ? decodeURIComponent(match[1]) : null;
}
function extractJwtFromHeader(req: IncomingMessage): string | null {
const auth = req.headers['authorization'];
return auth?.startsWith('Bearer ') ? auth.slice(7) : null;
}
function verifyJwt(token: string, secret: string): { sub: string; exp: number } | null {
function verifyJwt(token: string, secret: string): { sub: string; exp?: number } | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
if (header.alg !== 'HS256') return null;
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
if (!payload.sub || typeof payload.sub !== 'string') return null;
if (payload.exp && Date.now() / 1000 > payload.exp) return null;
const crypto = require('crypto');
const sigInput = `${parts[0]}.${parts[1]}`;
const expected = crypto.createHmac('sha256', secret).update(sigInput).digest('base64url');
if (expected !== parts[2]) return null;
return { sub: payload.sub, exp: payload.exp || 0 };
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
if (typeof decoded !== 'object' || !decoded.sub) return null;
return {
sub: String(decoded.sub),
exp: decoded.exp ? Number(decoded.exp) : undefined,
};
} catch {
return null;
}
@@ -154,7 +146,7 @@ export class AlertServer extends EventEmitter {
// JWT authentication
if (this.config.enableAuth) {
const token = extractJwtFromQuery(info.req.url || '') || extractJwtFromHeader(info.req);
const token = extractJwt(info.req);
if (!token) {
cb(false, 401, 'Missing JWT token');
return;
@@ -179,7 +171,7 @@ export class AlertServer extends EventEmitter {
* Handle new WebSocket connection
*/
private handleConnection(ws: WebSocket, req: IncomingMessage) {
const token = extractJwtFromQuery(req.url || '') || extractJwtFromHeader(req);
const token = extractJwt(req);
const payload = token ? verifyJwt(token, this.config.jwtSecret) : null;
const userId = payload?.sub || 'anonymous';