From 156379ab5433928f587507aff6e88dd73a8826c9 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Tue, 23 Dec 2025 15:22:26 +0100 Subject: [PATCH] feat(auth): close connection on unauthenticated socket connection --- src/chat/src/@types/socket.io.d.ts | 12 ++++++ src/chat/src/plugins/socket.ts | 50 ++++++++++++++++++++++- src/tic-tac-toe/src/@types/socket.io.d.ts | 12 ++++++ src/tic-tac-toe/src/plugins/socket.ts | 50 ++++++++++++++++++++++- 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/chat/src/@types/socket.io.d.ts create mode 100644 src/tic-tac-toe/src/@types/socket.io.d.ts diff --git a/src/chat/src/@types/socket.io.d.ts b/src/chat/src/@types/socket.io.d.ts new file mode 100644 index 0000000..38dedaa --- /dev/null +++ b/src/chat/src/@types/socket.io.d.ts @@ -0,0 +1,12 @@ +import { type UserId } from '@shared/database/mixin/user'; + +declare module 'socket.io' +{ + interface Socket { + authUser: { + id: UserId; + name: string; + guest: boolean; + } + } +}; diff --git a/src/chat/src/plugins/socket.ts b/src/chat/src/plugins/socket.ts index f32dec8..763f26a 100644 --- a/src/chat/src/plugins/socket.ts +++ b/src/chat/src/plugins/socket.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { JwtType } from '@shared/auth'; +import { UserId } from '@shared/database/mixin/user'; +import { isNullish } from '@shared/utils'; import type { FastifyInstance, FastifyPluginAsync, @@ -11,6 +15,21 @@ const F: ( ) => Omit & { io: Server } = (f) => f as Omit & { io: Server }; +function authenticateToken( + fastify: FastifyInstance, + token: string, +): { id: UserId; name: string; guest: boolean } { + const tok = fastify.jwt.verify(token); + if (tok.kind != 'auth') { + throw new Error('Token isn\'t correct type'); + } + const user = fastify.db.getUser(tok.who); + if (isNullish(user)) { + throw new Error('User not found'); + } + return { id: user.id, name: user.name, guest: user.guest }; +} + const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => { function defaultPreClose(done: HookHandlerDoneFunction) { F(fastify).io.local.disconnectSockets(true); @@ -20,6 +39,36 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => { 'io', new Server(fastify.server, { path: '/api/chat/socket.io' }), ); + F(fastify).io.use((socket, next) => { + const cookieHeader = socket.request.headers.cookie; + + if (!cookieHeader) { + throw new Error('Missing token cookie'); + } + + const cookies = Object.fromEntries( + cookieHeader.split(';').map((c) => { + const [k, v] = c.trim().split('='); + return [k, v]; + }), + ); + + if (!cookies.token) { + throw new Error('Missing token cookie'); + } + try { + socket.authUser = authenticateToken(fastify, cookies.token); + next(); + } + catch (e: any) { + next({ + name: 'Unauthorized', + message: e.message, + data: { status: 401 }, + }); + } + }); + fastify.addHook('preClose', defaultPreClose); fastify.addHook('onClose', (instance: FastifyInstance, done) => { F(instance).io.close(); @@ -28,4 +77,3 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => { }); export default fastifySocketIO; - diff --git a/src/tic-tac-toe/src/@types/socket.io.d.ts b/src/tic-tac-toe/src/@types/socket.io.d.ts new file mode 100644 index 0000000..38dedaa --- /dev/null +++ b/src/tic-tac-toe/src/@types/socket.io.d.ts @@ -0,0 +1,12 @@ +import { type UserId } from '@shared/database/mixin/user'; + +declare module 'socket.io' +{ + interface Socket { + authUser: { + id: UserId; + name: string; + guest: boolean; + } + } +}; diff --git a/src/tic-tac-toe/src/plugins/socket.ts b/src/tic-tac-toe/src/plugins/socket.ts index f485775..64bf647 100644 --- a/src/tic-tac-toe/src/plugins/socket.ts +++ b/src/tic-tac-toe/src/plugins/socket.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { JwtType } from '@shared/auth'; +import { UserId } from '@shared/database/mixin/user'; +import { isNullish } from '@shared/utils'; import type { FastifyInstance, FastifyPluginAsync, @@ -11,6 +15,21 @@ const F: ( ) => Omit & { io: Server } = (f) => f as Omit & { io: Server }; +function authenticateToken( + fastify: FastifyInstance, + token: string, +): { id: UserId; name: string; guest: boolean } { + const tok = fastify.jwt.verify(token); + if (tok.kind != 'auth') { + throw new Error('Token isn\'t correct type'); + } + const user = fastify.db.getUser(tok.who); + if (isNullish(user)) { + throw new Error('User not found'); + } + return { id: user.id, name: user.name, guest: user.guest }; +} + const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => { function defaultPreClose(done: HookHandlerDoneFunction) { F(fastify).io.local.disconnectSockets(true); @@ -20,6 +39,36 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => { 'io', new Server(fastify.server, { path: '/api/ttt/socket.io' }), ); + F(fastify).io.use((socket, next) => { + const cookieHeader = socket.request.headers.cookie; + + if (!cookieHeader) { + throw new Error('Missing token cookie'); + } + + const cookies = Object.fromEntries( + cookieHeader.split(';').map((c) => { + const [k, v] = c.trim().split('='); + return [k, v]; + }), + ); + + if (!cookies.token) { + throw new Error('Missing token cookie'); + } + try { + socket.authUser = authenticateToken(fastify, cookies.token); + next(); + } + catch (e: any) { + next({ + name: 'Unauthorized', + message: e.message, + data: { status: 401 }, + }); + } + }); + fastify.addHook('preClose', defaultPreClose); fastify.addHook('onClose', (instance: FastifyInstance, done) => { F(instance).io.close(); @@ -28,4 +77,3 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => { }); export default fastifySocketIO; -