WebSocket com autenticação JWT em Node.js

    WebSockets são conexões persistentes bidirecionais — o servidor pode enviar mensagens ao cliente a qualquer momento. Autenticar essas conexões é diferente de requisições HTTP: o token precisa ser validado no handshake inicial, e a conexão deve ser encerrada se o token expirar durante a sessão.

    WebSocket autenticado com biblioteca ws

    Validar JWT no handshake HTTP antes de fazer o upgrade para WebSocket:

    typescript
    // npm install ws @types/ws
    
    import { WebSocketServer, WebSocket } from 'ws'
    import { IncomingMessage } from 'http'
    import { verificarAccessToken, type TokenPayload } from './lib/jwt'
    
    // Estender WebSocket para guardar dados do usuário:
    interface WebSocketAutenticado extends WebSocket {
      usuario?: TokenPayload
      isAlive?: boolean
    }
    
    const wss = new WebSocketServer({ port: 8080 })
    
    // Autenticar no handshake:
    wss.on('connection', async (ws: WebSocketAutenticado, req: IncomingMessage) => {
      // Token pode vir como query param (impossível usar header em WebSocket browser nativo):
      // ws://api.seudominio.com.br/ws?token=eyJ...
      const url = new URL(req.url!, `http://${req.headers.host}`)
      const token = url.searchParams.get('token')
    
      if (!token) {
        ws.close(4001, 'Token não fornecido')
        return
      }
    
      try {
        ws.usuario = await verificarAccessToken(token)
      } catch {
        ws.close(4001, 'Token inválido ou expirado')
        return
      }
    
      console.log(`WebSocket conectado: ${ws.usuario.email}`)
    
      ws.isAlive = true
      ws.on('pong', () => { ws.isAlive = true })
    
      ws.on('message', (data) => {
        const msg = JSON.parse(data.toString())
        handleMensagem(ws, msg)
      })
    
      ws.on('close', () => {
        console.log(`WebSocket desconectado: ${ws.usuario?.email}`)
      })
    
      // Confirmar conexão:
      ws.send(JSON.stringify({ type: 'connected', userId: ws.usuario.userId }))
    })
    
    // Heartbeat para detectar conexões mortas:
    const heartbeat = setInterval(() => {
      wss.clients.forEach((ws: WebSocketAutenticado) => {
        if (!ws.isAlive) return ws.terminate()
        ws.isAlive = false
        ws.ping()
      })
    }, 30_000)
    
    wss.on('close', () => clearInterval(heartbeat))

    Socket.IO com autenticação JWT

    Middleware de autenticação e rooms por usuário no Socket.IO:

    typescript
    // npm install socket.io
    
    import { Server } from 'socket.io'
    import { verificarAccessToken } from './lib/jwt'
    
    const io = new Server(httpServer, {
      cors: {
        origin: ['https://seudominio.com.br'],
        credentials: true,
      },
    })
    
    // Middleware de autenticação (executa antes de cada conexão):
    io.use(async (socket, next) => {
      const token =
        socket.handshake.auth.token ||          // enviado pelo cliente via auth option
        socket.handshake.headers.authorization?.slice(7)
    
      if (!token) return next(new Error('Token não fornecido'))
    
      try {
        socket.data.usuario = await verificarAccessToken(token)
        next()
      } catch {
        next(new Error('Token inválido'))
      }
    })
    
    io.on('connection', (socket) => {
      const usuario = socket.data.usuario
    
      // Entrar em rooms pessoais (receber notificações diretas):
      socket.join(`user:${usuario.userId}`)
    
      // Entrar em rooms de organização:
      socket.join(`org:${usuario.orgId}`)
    
      console.log(`Socket conectado: ${usuario.email} [${socket.id}]`)
    
      // Escutar eventos do cliente:
      socket.on('mensagem:enviar', async (data) => {
        // Emitir para todos na room:
        io.to(`org:${usuario.orgId}`).emit('mensagem:nova', {
          de: usuario.email,
          conteudo: data.conteudo,
          timestamp: Date.now(),
        })
      })
    
      socket.on('disconnect', () => {
        console.log(`Socket desconectado: ${usuario.email}`)
      })
    })
    
    // Enviar notificação para usuário específico do servidor:
    export function notificarUsuario(userId: string, evento: string, dados: unknown) {
      io.to(`user:${userId}`).emit(evento, dados)
    }

    Cliente frontend com reconnect e refresh de token

    Reconectar automaticamente com token renovado ao receber erro 4001:

    typescript
    // frontend/websocket.ts
    import { refreshAccessToken } from './auth'
    
    let ws: WebSocket | null = null
    let token = ''
    let reconnectTimer: ReturnType<typeof setTimeout> | null = null
    let reconnectDelay = 1000
    
    export async function conectarWebSocket() {
      token = getAccessTokenFromMemoria()
    
      ws = new WebSocket(`wss://api.seudominio.com.br/ws?token=${token}`)
    
      ws.addEventListener('open', () => {
        console.log('WebSocket conectado')
        reconnectDelay = 1000  // resetar delay ao conectar com sucesso
      })
    
      ws.addEventListener('message', (event) => {
        const msg = JSON.parse(event.data)
        handleMensagem(msg)
      })
    
      ws.addEventListener('close', async (event) => {
        console.log(`WebSocket fechado: ${event.code} ${event.reason}`)
    
        if (event.code === 4001) {
          // Token expirado — tentar renovar:
          try {
            token = await refreshAccessToken()
            salvarAccessTokenNaMemoria(token)
            conectarWebSocket()  // reconectar imediatamente com novo token
            return
          } catch {
            // Refresh falhou — redirecionar para login:
            window.location.href = '/login'
            return
          }
        }
    
        // Reconectar com backoff exponencial:
        if (reconnectTimer) clearTimeout(reconnectTimer)
        reconnectTimer = setTimeout(() => {
          reconnectDelay = Math.min(reconnectDelay * 2, 30_000)
          conectarWebSocket()
        }, reconnectDelay)
      })
    
      ws.addEventListener('error', (error) => {
        console.error('WebSocket erro:', error)
      })
    
      return ws
    }
    
    export function enviarMensagem(tipo: string, dados: unknown) {
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: tipo, ...dados as object }))
      }
    }

    Broadcast e mensagens direcionadas

    Padrões para enviar mensagens para usuários, grupos e todos:

    typescript
    // Padrões de envio no Socket.IO:
    
    // 1. Para um usuário específico (de qualquer parte do código):
    io.to(`user:${userId}`).emit('notificacao', { mensagem: 'Novo pedido criado!' })
    
    // 2. Para todos em uma organização:
    io.to(`org:${orgId}`).emit('pedido:atualizado', { pedidoId, status: 'aprovado' })
    
    // 3. Broadcast para todos (exceto o remetente):
    socket.broadcast.emit('usuario:online', { nome: usuario.nome })
    
    // 4. Para todos (incluindo o remetente):
    io.emit('manutencao:aviso', { mensagem: 'Sistema em manutenção em 10 min' })
    
    // 5. Para uma sala específica (ex: chat de suporte):
    io.to(`ticket:${ticketId}`).emit('mensagem:nova', dados)
    
    // Usando com Express — enviar notificação em rota HTTP:
    app.post('/api/pedidos', autenticar, async (req, res) => {
      const pedido = await criarPedido(req.body)
    
      // Notificar o criador do pedido em tempo real:
      notificarUsuario(req.usuario!.userId, 'pedido:criado', { pedidoId: pedido.id })
    
      // Notificar administradores:
      io.to('role:admin').emit('pedido:novo', { pedido })
    
      res.status(201).json(pedido)
    })

    WebSocket atrás de Nginx

    Configurar proxy WebSocket no Nginx:

    bash
    # /etc/nginx/sites-enabled/api.conf
    
    server {
        listen 443 ssl;
        server_name api.seudominio.com.br;
    
        # Proxy para API HTTP:
        location /api/ {
            proxy_pass http://127.0.0.1:3000;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    
        # Proxy WebSocket (upgrade necessário):
        location /ws {
            proxy_pass http://127.0.0.1:8080;
            proxy_http_version 1.1;
    
            # Headers obrigatórios para WebSocket:
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
    
            # Timeout longo para conexões persistentes:
            proxy_read_timeout 3600s;   # 1 hora
            proxy_send_timeout 3600s;
    
            # Desabilitar buffering para WebSocket:
            proxy_buffering off;
        }
    
        # Socket.IO usa /socket.io/ por padrão:
        location /socket.io/ {
            proxy_pass http://127.0.0.1:3000/socket.io/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 3600s;
            proxy_buffering off;
        }
    }

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes