Principal Conexões com canais externos (caixas de entrada) Sessão compartilhada do Web Widget entre abas do navegador

Sessão compartilhada do Web Widget entre abas do navegador

Última atualização em Apr 13, 2026

1. Introdução

Este guia explica como manter a mesma sessão de usuário do web widget Cloud Chat em duas ou mais abas do navegador, sem exigir que cada aba esteja autenticada na aplicação host.

Cenário típico: o usuário abre seu site em duas abas e inicia uma conversa pelo widget em uma delas. Na segunda aba, o widget deve reconhecer o mesmo usuário e exibir o mesmo histórico de conversas.

Quando usar este guia

  • Sua aplicação permite múltiplas abas abertas simultaneamente.

  • Você já utiliza (ou deseja utilizar) a Validação de Identidade (Identity Validation) do widget.

  • Você quer que o widget identifique o mesmo contato em todas as abas, sem exigir login em cada uma.


2. Pré-requisitos


3. Como funciona (explicação simplificada)

Pense assim:

  1. user_id é o "nome no crachá" — identifica quem é o usuário.

  2. identifier_hash é o "carimbo de autenticidade" — prova que o crachá é verdadeiro.

  3. O Cloud Chat confere o carimbo (usando uma chave secreta que só o servidor conhece) antes de liberar o acesso ao histórico.

Fluxo entre abas

Ponto-chave: cada aba precisa chamar setUser() independentemente. O cookie cw_user_* é compartilhado automaticamente entre abas (mesmo domínio), mas o authToken interno do widget é por instância (por aba). Por isso, ambas as abas precisam ter acesso ao user_id e identifier_hash para inicializar corretamente.


4. Implementação

4.1 Opção Recomendada: Endpoint server-side

Esta é a opção mais segura. O identifier_hash nunca é armazenado no navegador — é obtido sob demanda a partir de um endpoint protegido pela autenticação da sua aplicação.

Backend (exemplo Node.js/Express)

const crypto = require('crypto');

// Rota protegida por autenticação da sua aplicação

app.get('/api/widget-identity', authMiddleware, (req, res) => {

  const userId = String(req.user.id); // ID do usuário logado

  const hmacToken = process.env.CLOUDCHAT_HMAC_TOKEN; // Chave secreta do widget

  const identifierHash = crypto

    .createHmac('sha256', hmacToken)

    .update(userId)

    .digest('hex');

  res.json({

    user_id: userId,

    identifier_hash: identifierHash,

    name: req.user.name,

    email: req.user.email,

  });

});

Backend (exemplo Ruby on Rails)

# app/controllers/api/widget_identity_controller.rb

class Api::WidgetIdentityController < ApplicationController

  before_action :authenticate_user!

  def show

    user_id = current_user.id.to_s

    hmac_token = ENV['CLOUDCHAT_HMAC_TOKEN']

    identifier_hash = OpenSSL::HMAC.hexdigest('sha256', hmac_token, user_id)

    render json: {

      user_id: user_id,

      identifier_hash: identifier_hash,

      name: current_user.name,

      email: current_user.email

    }

  end

end

Backend (exemplo Python/Django)

import hmac

import hashlib

from django.http import JsonResponse

from django.contrib.auth.decorators import login_required

from django.conf import settings

@login_required

def widget_identity(request):

    user_id = str(request.user.id)

    identifier_hash = hmac.new(

        settings.CLOUDCHAT_HMAC_TOKEN.encode(),

        user_id.encode(),

        hashlib.sha256

    ).hexdigest()

    return JsonResponse({

        'user_id': user_id,

        'identifier_hash': identifier_hash,

        'name': request.user.get_full_name(),

        'email': request.user.email,

    })

Frontend (qualquer aba)

<script>

  window.Cloud ChatSettings = {

    // ... configurações do widget

  };

  window.addEventListener('Cloud Chat:ready', async function () {

    try {

      const response = await fetch('/api/widget-identity', {

        credentials: 'include', // Envia cookies de sessão

      });

      if (!response.ok) {

        console.error('Falha ao obter identidade do widget');

        return;

      }

      const { user_id, identifier_hash, name, email } = await response.json();

      window.$Cloud Chat.setUser(user_id, {

        identifier_hash: identifier_hash,

        name: name,

        email: email,

      });

    } catch (error) {

      console.error('Erro ao configurar usuário do widget:', error);

    }

  });

</script>

Vantagens:

  • O hmac_token nunca sai do servidor.

  • O identifier_hash nunca é persistido no navegador.

  • Cada aba obtém dados frescos, garantindo consistência.

  • Se o usuário fizer logout, o endpoint retorna 401 e o widget não é identificado.


4.2 Opção Alternativa: localStorage (com ressalvas)

Use esta opção somente se a sua aplicação não possui um backend disponível para fornecer o identifier_hash (por exemplo, SPAs puramente estáticas).

Aviso de Segurança

O localStorage é acessível por qualquer script JavaScript executado no mesmo domínio. Se o seu site for vulnerável a XSS (Cross-Site Scripting), um atacante pode ler o identifier_hash armazenado. Veja a seção 5 (Segurança) para entender os riscos.

Aba que gera os dados (ex: página de login)

// Após autenticação, seu backend retorna user_id e identifier_hash

const userData = {

  user_id: '12345',

  identifier_hash: 'abc123def456...', // Gerado pelo seu backend

  name: 'Maria Silva',

  email: '[email protected]',

};

localStorage.setItem('Cloud Chat_user', JSON.stringify(userData));

Qualquer aba que carrega o widget

window.addEventListener('Cloud Chat:ready', function () {

  const stored = localStorage.getItem('Cloud Chat_user');

  if (!stored) return;

  try {

    const { user_id, identifier_hash, name, email } = JSON.parse(stored);

    window.$Cloud Chat.setUser(user_id, {

      identifier_hash: identifier_hash,

      name: name,

      email: email,

    });

  } catch (e) {

    console.error('Dados do widget inválidos no localStorage');

    localStorage.removeItem('Cloud Chat_user');

  }

});

Ao fazer logout

localStorage.removeItem('Cloud Chat_user');

window.$Cloud Chat.reset();

4.3 Opção Avançada: sessionStorage + BroadcastChannel

Esta opção combina a não persistência do sessionStorage com a sincronização entre abas via BroadcastChannel API. Os dados existem apenas enquanto pelo menos uma aba está aberta.

Inicialização (em todas as abas)

const CHANNEL_NAME = 'Cloud Chat-session';

const STORAGE_KEY = 'Cloud Chat_user';

const channel = new BroadcastChannel(CHANNEL_NAME);

// Ao receber dados de outra aba

channel.onmessage = (event) => {

  if (event.data.type === 'session-data') {

    sessionStorage.setItem(STORAGE_KEY, JSON.stringify(event.data.payload));

    initWidget(event.data.payload);

  }

  if (event.data.type === 'session-request') {

    // Outra aba pediu os dados — enviar se tivermos

    const stored = sessionStorage.getItem(STORAGE_KEY);

    if (stored) {

      channel.postMessage({ type: 'session-data', payload: JSON.parse(stored) });

    }

  }

  if (event.data.type === 'session-clear') {

    sessionStorage.removeItem(STORAGE_KEY);

    window.$Cloud Chat.reset();

  }

};

function initWidget(userData) {

  window.$Cloud Chat.setUser(userData.user_id, {

    identifier_hash: userData.identifier_hash,

    name: userData.name,

    email: userData.email,

  });

}

window.addEventListener('Cloud Chat:ready', function () {

  // Verificar se já temos dados nesta aba

  const stored = sessionStorage.getItem(STORAGE_KEY);

  if (stored) {

    initWidget(JSON.parse(stored));

    return;

  }

  // Pedir dados para outras abas

  channel.postMessage({ type: 'session-request' });

  // Timeout: se nenhuma aba responder em 2s, esta é a primeira aba

  // (o usuário precisará fazer login normalmente)

});

Ao autenticar (aba principal)

// Após receber user_id e identifier_hash do backend

const userData = { user_id, identifier_hash, name, email };

sessionStorage.setItem(STORAGE_KEY, JSON.stringify(userData));

channel.postMessage({ type: 'session-data', payload: userData });

initWidget(userData);

Ao fazer logout

sessionStorage.removeItem(STORAGE_KEY);

channel.postMessage({ type: 'session-clear' });

window.$Cloud Chat.reset();

Vantagens:

  • Dados não persistem após fechar todas as abas.

  • Sincronização em tempo real entre abas.

  • Sem dependência de cookies acessíveis por JS.

Limitações:

  • BroadcastChannel não é suportado no IE11 (mas funciona em todos os navegadores modernos).

  • Se o usuário fechar todas as abas e reabrir, precisará autenticar novamente.


5. Segurança

O que NUNCA fazer

Nunca exponha o hmac_token (chave secreta) no frontend. Esta chave é usada pelo servidor para gerar o identifier_hash. Se um atacante obtiver o hmac_token, ele pode gerar hashes válidos para qualquer user_id, personificando qualquer usuário.

// ERRADO — NUNCA faça isso

const hmacToken = 'sua-chave-secreta-aqui';

const hash = CryptoJS.HmacSHA256(userId, hmacToken).toString();

Riscos de armazenar identifier_hash no localStorage

Contextualização do risco

O identifier_hash por si só não é um segredo de alto valor:

  • Ele é uma assinatura (HMAC) do user_id, não um token de acesso.

  • Um atacante precisa do user_id correspondente E a validação HMAC estar ativa para personificar alguém.

  • O identifier_hash não concede acesso a sistemas além do widget de chat.

  • O impacto de um vazamento é limitado: o atacante poderia, no máximo, abrir conversas como aquele usuário no widget.

Ainda assim, a opção de endpoint server-side é preferível por eliminar completamente a exposição.

Tabela comparativa de abordagens de armazenamento

Boas práticas

  1. Proteja-se contra XSS — é a ameaça número 1 para qualquer dado no navegador. Use Content Security Policy (CSP), sanitize inputs e mantenha dependências atualizadas.

  2. Gere identifier_hash no servidor — nunca no frontend.

  3. Use HTTPS — sempre. Sem exceções.

  4. Limite a superfície de ataque — se possível, carregue o widget apenas em páginas onde ele é necessário.

  5. Limpe dados ao fazer logout — remova localStorage/sessionStorage e chame $Cloud Chat.reset().

Referências:


6. Reset de sessão

Use window.$Cloud Chat.reset() para limpar a sessão do widget. Internamente, o reset:

  1. Fecha o chat se estiver aberto.

  2. Remove o cookie cw_conversation (token de sessão da conversa).

  3. Remove o cookie cw_user_{websiteToken} (identidade do usuário).

  4. Recarrega o iframe do widget, iniciando uma sessão limpa.

Quando usar

Exemplo: logout completo


function onLogout() {

  // 1. Limpar dados armazenados (se estiver usando localStorage)

  localStorage.removeItem('Cloud Chat_user');

  // 2. Resetar o widget

  if (window.$Cloud Chat) {

    window.$Cloud Chat.reset();

  }

  // 3. Redirecionar para página de login

  window.location.href = '/login';

}

7. Troubleshooting

Problema: Widget mostra "Iniciar nova conversa" em vez do histórico

Causa provável: setUser() não foi chamado, ou foi chamado com identifier_hash inválido.

Solução:

  1. Verifique no console do navegador se há erros de rede (HTTP 401 no endpoint /widget/contact).

  2. Confirme que o identifier_hash foi gerado com o hmac_token correto (do painel do widget).

  3. Confirme que o user_id passado para setUser() é o mesmo usado para gerar o hash.

Problema: Abas mostram usuários diferentes

Causa provável: cada aba está chamando setUser() com dados diferentes.

Solução:

  1. Se estiver usando endpoint server-side, confirme que ambas as abas chamam o mesmo endpoint e que o usuário logado é o mesmo.

  2. Se estiver usando localStorage, verifique que os dados foram gravados antes da segunda aba tentar lê-los.

Problema: Após logout, o widget ainda mostra dados do usuário anterior

Causa provável: $Cloud Chat.reset() não foi chamado, ou os cookies não foram limpos.

Solução:

  1. Chame window.$Cloud Chat.reset() antes ou durante o processo de logout.

  2. Limpe localStorage/sessionStorage se estiver armazenando dados lá.

  3. Verifique se o cookie cw_user_* foi removido (DevTools > Application > Cookies).

Problema: setUser() lança erro "Identifier should be a string or a number"

Causa: o user_id está sendo passado como undefined, null ou objeto.

Solução: certifique-se de que o valor é uma string ou número. Se vier de uma API, faça a conversão:


window.$Cloud Chat.setUser(String(userData.user_id), { ... });

Problema: setUser() lança erro "User object should have one of the keys..."

Causa: o objeto de usuário não contém nenhuma das chaves obrigatórias.

Solução: inclua ao menos uma das chaves: name, email ou avatar_url.


window.$Cloud Chat.setUser(userId, {

  identifier_hash: hash,

  name: 'Nome do Usuário', // Obrigatório ter ao menos um: name, email ou avatar_url

});

Problema: BroadcastChannel não sincroniza entre abas

Causa provável: as abas estão em domínios diferentes (ex: app.exemplo.com vs www.exemplo.com).

Solução: BroadcastChannel só funciona entre páginas do mesmo domínio (same-origin). Certifique-se de que todas as abas usam exatamente a mesma URL base.