diff --git a/content/websockets/guards.md b/content/websockets/guards.md index 51a44c6228..099c35db76 100644 --- a/content/websockets/guards.md +++ b/content/websockets/guards.md @@ -24,3 +24,104 @@ handleEvent(client, data) { return { event, data }; } ``` + +#### Authenticating connections + +Guards bound with `@UseGuards()` protect `@SubscribeMessage()` handlers, but the initial Socket.IO connection is established in `handleConnection()` before those handlers run. To reject unauthorized clients at connect time, validate credentials inside `OnGatewayConnection.handleConnection()` and call `client.disconnect()` when validation fails. + +With socket.io, clients can pass a JWT in `handshake.auth` (recommended) or the `Authorization` header: + +```typescript +@@filename(events.gateway) +import { ConnectedSocket, OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets'; +import { Socket } from 'socket.io'; +import { JwtService } from '@nestjs/jwt'; + +@WebSocketGateway({ namespace: 'events' }) +export class EventsGateway implements OnGatewayConnection { + constructor(private readonly jwtService: JwtService) {} + + async handleConnection(@ConnectedSocket() client: Socket) { + const token = this.extractToken(client); + + if (!token) { + client.disconnect(); + return; + } + + try { + const payload = await this.jwtService.verifyAsync(token); + await client.join(`user:${payload.sub}`); + } catch { + client.disconnect(); + } + } + + private extractToken(client: Socket): string | undefined { + const authToken = client.handshake.auth?.token; + + if (typeof authToken === 'string') { + return authToken.replace(/^Bearer\s+/i, ''); + } + + const authorization = client.handshake.headers.authorization; + + if (typeof authorization === 'string') { + return authorization.replace(/^Bearer\s+/i, ''); + } + } +} +@@switch +import { WebSocketGateway } from '@nestjs/websockets'; + +@WebSocketGateway({ namespace: 'events' }) +export class EventsGateway { + constructor(jwtService) { + this.jwtService = jwtService; + } + + async handleConnection(client) { + const token = this.extractToken(client); + + if (!token) { + client.disconnect(); + return; + } + + try { + const payload = await this.jwtService.verifyAsync(token); + await client.join(`user:${payload.sub}`); + } catch { + client.disconnect(); + } + } + + extractToken(client) { + const authToken = client.handshake.auth?.token; + + if (typeof authToken === 'string') { + return authToken.replace(/^Bearer\s+/i, ''); + } + + const authorization = client.handshake.headers.authorization; + + if (typeof authorization === 'string') { + return authorization.replace(/^Bearer\s+/i, ''); + } + } +} +``` + +Client example: + +```typescript +import { io } from 'socket.io-client'; + +const socket = io('http://localhost:3000/events', { + auth: { + token: 'Bearer ', + }, +}); +``` + +> info **Hint** Method-scoped guards still apply to incoming messages after the connection is accepted. Use both patterns when you need to protect the handshake and individual events. diff --git a/src/app/homepage/pages/websockets/guards/guards.component.ts b/src/app/homepage/pages/websockets/guards/guards.component.ts index f411790d38..26b006245e 100644 --- a/src/app/homepage/pages/websockets/guards/guards.component.ts +++ b/src/app/homepage/pages/websockets/guards/guards.component.ts @@ -4,6 +4,7 @@ import { RouterLink } from '@angular/router'; import { HeaderAnchorDirective } from '../../../../shared/directives/header-anchor.directive'; import { CopyButtonComponent } from '../../../../shared/components/copy-button/copy-button.component'; import { TabsComponent } from '../../../../shared/components/tabs/tabs.component'; +import { ExtensionPipe } from '../../../../shared/pipes/extension.pipe'; @Component({ selector: 'app-guards', @@ -15,6 +16,7 @@ import { TabsComponent } from '../../../../shared/components/tabs/tabs.component HeaderAnchorDirective, CopyButtonComponent, TabsComponent, + ExtensionPipe, ], }) export class WsGuardsComponent extends BasePageComponent {}