Skip to content

Commit afb6669

Browse files
feat: implement Redis-based temp token exchange for better security
1 parent dad52f9 commit afb6669

File tree

17 files changed

+767
-281
lines changed

17 files changed

+767
-281
lines changed

collaboration-service/collaboration-service/package-lock.json

Lines changed: 119 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

collaboration-service/collaboration-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"cors": "^2.8.5",
1515
"dotenv": "^16.4.5",
1616
"express": "^4.19.2",
17+
"jsonwebtoken": "^9.0.2",
1718
"node-fetch": "^3.3.2",
1819
"redis": "^5.8.3",
1920
"uuid": "^9.0.1",

collaboration-service/collaboration-service/src/ws/gateway.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { applyOp } from "../services/documentService.js";
33
import { touchPresence } from "../services/presenceService.js";
44
import { generateAIResponse } from "../services/aiService.js";
55
import { z } from "zod";
6+
import { verifyCollabTokenOrThrow } from "./tokenHelper.js";
67

78
// WebSocket rooms (in-memory connection maps; lightweight, ephemeral)
89
const wsRooms = new Map();
@@ -28,18 +29,37 @@ export function initGateway(wss) {
2829
const url = new URL(req.url, `http://${req.headers.host}`);
2930
let sessionId = url.searchParams.get("sessionId") || "";
3031
let userId = url.searchParams.get("userId") || "";
32+
let token = url.searchParams.get("token") || "";
3133

3234
console.log("[WS] Extracted params - sessionId:", sessionId, "userId:", userId);
3335

34-
if (!sessionId) sessionId = String(req.headers["x-session-id"] || "");
35-
if (!userId) userId = String(req.headers["x-user-id"] || "");
36-
37-
if (!sessionId || !userId) {
38-
console.log("[WS] Missing params - closing connection");
39-
return ws.close(1008, "missing params");
36+
// if (!sessionId) sessionId = String(req.headers["x-session-id"] || "");
37+
// if (!userId) userId = String(req.headers["x-user-id"] || "");
38+
// if (!token) token = String(req.headers["x-auth-token"] || "");
39+
40+
// if (!sessionId || !userId) {
41+
// console.log("[WS] Missing params - closing connection");
42+
// return ws.close(1008, "missing params");
43+
// } else if (!token) {
44+
// console.log("[WS] Missing auth token - closing connection");
45+
// return ws.close(1008, "missing auth token");
46+
// }
47+
48+
if (!sessionId) {
49+
sessionId = String(req.headers["x-session-id"] || "");
50+
console.log("[WS] Missing sessionId - closing connection");
51+
return ws.close(1008, "missing sessionId");
52+
} else if (!userId) {
53+
userId = String(req.headers["x-user-id"] || "");
54+
console.log("[WS] Missing userId - closing connection");
55+
return ws.close(1008, "missing userId");
56+
} else if (!token) {
57+
token = String(req.headers["x-auth-token"] || "");
58+
console.log("[WS] Missing auth token - closing connection");
59+
return ws.close(1008, "missing auth token");
4060
}
4161

42-
// Authorization check
62+
// User authorization check
4363
try {
4464
console.log("[WS] Checking authorization for user:", userId, "session:", sessionId);
4565
const allowed = await redisRepo.sIsMember(
@@ -67,11 +87,23 @@ export function initGateway(wss) {
6787
return ws.close(1011, "auth check error");
6888
}
6989

90+
// Token authorization check
91+
try {
92+
const dec = verifyCollabTokenOrThrow(token, sessionId);
93+
if (dec.userId) userId = String(dec.userId);
94+
ws.isAuthenticated = true;
95+
ws.userId = userId;
96+
} catch (err) {
97+
console.warn("[WS] JWT verification failed:", err);
98+
return ws.close(1008, "invalid token");
99+
}
100+
70101
if (!wsRooms.has(sessionId)) wsRooms.set(sessionId, new Set());
71102
wsRooms.get(sessionId).add(ws);
72103

73104
console.log(`[WS] ${userId} connected to ${sessionId}`);
74105

106+
// Send initial state
75107
const doc = (await redisRepo.getJson(`collab:document:${sessionId}`)) || { version: 0, text: "" };
76108
const chat = (await redisRepo.getList(`collab:chat:${sessionId}`)) || [];
77109
const aiChat = (await redisRepo.getList(`collab:ai-chat:${sessionId}`)) || [];
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import jwt from "jsonwebtoken";
2+
3+
function verifyCollabTokenOrThrow(token, expectedSessionId) {
4+
const secret = process.env.JWT_ACCESS_TOKEN_SECRET;
5+
if (!secret) throw new Error("JWT_ACCESS_TOKEN_SECRET not configured");
6+
7+
const decoded = jwt.verify(token, secret); // will throw if invalid/expired
8+
9+
// Optional hardening: assert audience and session binding if present
10+
if (decoded.aud && decoded.aud !== "collab") {
11+
throw new Error("invalid audience");
12+
}
13+
if (decoded.sessionId && expectedSessionId && decoded.sessionId !== expectedSessionId) {
14+
throw new Error("session mismatch");
15+
}
16+
return decoded;
17+
}
18+
19+
export { verifyCollabTokenOrThrow };

collaboration-service/collaboration-service/src/ws/yjsGateway.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// src/ws/yjsGateway.js
22
import * as Y from "yjs";
33
import { redisRepo } from "../repos/redisRepo.js";
4+
import { verifyCollabTokenOrThrow } from "./tokenHelper.js";
45

56
/**
67
* Rooms: sessionId -> { doc: Y.Doc, conns: Set<WebSocket> }
@@ -95,8 +96,22 @@ export function initYjsGateway(yws) {
9596
yws.on("connection", async (ws, req) => {
9697
console.log("[YJS Gateway] New connection:", req.url);
9798
const { searchParams } = new URL(req.url, `http://${req.headers.host}`);
98-
const sessionId = searchParams.get("sessionId") || "default";
99-
const userId = searchParams.get("userId") || "anon";
99+
const sessionId = searchParams.get("sessionId") || "";
100+
let userId = searchParams.get("userId") || "";
101+
const token = searchParams.get("token") || "";
102+
103+
// Token authorization check
104+
try {
105+
if (!token) throw new Error("missing token");
106+
const dec = verifyCollabTokenOrThrow(token, sessionId);
107+
if (dec.userId) userId = String(dec.userId);
108+
ws.isAuthenticated = true;
109+
ws.userId = userId;
110+
} catch (err) {
111+
console.warn("[YJS Gateway] JWT verification failed:", err.message);
112+
ws.close(1008, "Unauthorized");
113+
return;
114+
}
100115

101116
const room = await getRoom(sessionId);
102117
room.conns.add(ws);

0 commit comments

Comments
 (0)