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:
-
user_idé o "nome no crachá" — identifica quem é o usuário. -
identifier_hashé o "carimbo de autenticidade" — prova que o crachá é verdadeiro. -
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_tokennunca sai do servidor. -
O
identifier_hashnunca é 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 oidentifier_hasharmazenado. 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:
-
BroadcastChannelnã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_idcorrespondente E a validação HMAC estar ativa para personificar alguém. -
O
identifier_hashnã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
-
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.
-
Gere
identifier_hashno servidor — nunca no frontend. -
Use HTTPS — sempre. Sem exceções.
-
Limite a superfície de ataque — se possível, carregue o widget apenas em páginas onde ele é necessário.
-
Limpe dados ao fazer logout — remova
localStorage/sessionStoragee 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:
-
Fecha o chat se estiver aberto.
-
Remove o cookie
cw_conversation(token de sessão da conversa). -
Remove o cookie
cw_user_{websiteToken}(identidade do usuário). -
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:
-
Verifique no console do navegador se há erros de rede (HTTP 401 no endpoint
/widget/contact). -
Confirme que o
identifier_hashfoi gerado com ohmac_tokencorreto (do painel do widget). -
Confirme que o
user_idpassado parasetUser()é 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:
-
Se estiver usando endpoint server-side, confirme que ambas as abas chamam o mesmo endpoint e que o usuário logado é o mesmo.
-
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:
-
Chame
window.$Cloud Chat.reset()antes ou durante o processo de logout. -
Limpe localStorage/sessionStorage se estiver armazenando dados lá.
-
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.