Como compartilhar a sessão do Web Widget entre abas do navegador
Quando usar
- O usuário abre o seu site em múltiplas abas e você quer que o widget reconheça a mesma identidade em todas
- Sua aplicação permite múltiplas abas abertas simultaneamente
- Você já usa (ou quer usar) Validação de Identidade do widget e quer evitar exigir login em cada aba
Pré-requisitos
- Web Widget já configurado com Validação de Identidade (Identity Validation) habilitada
- user_id único por usuário no seu sistema
- Backend disponível para gerar identifier_hash (opção recomendada)
- Web Widget instalado — ver Como instalar o Web Widget do Cloud Chat no site
Sobre este artigo
Este guia explica como manter a mesma sessão de usuário do Web Widget Cloud Chat em duas ou mais abas, sem exigir que
cada aba esteja autenticada.
Cenário típico: o usuário abre o 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.
Como funciona (explicação simplificada)
1. user_id = "nome no crachá" — identifica quem é o usuário
2. identifier_hash = "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 acesso ao
histórico
Fluxo entre abas
:::info 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.
:::
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 (Node.js / Express)
const crypto = require('crypto');
app.get('/api/widget-identity', authMiddleware, (req, res) => {
const userId = String(req.user.id);
const hmacToken = process.env.CLOUDCHAT_HMAC_TOKEN;
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 (Ruby on Rails)
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 (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.cloudchatSettings = {
// ... configurações do widget
};
window.addEventListener('cloudchat:ready', async function () {
try {
const response = await fetch('/api/widget-identity', {
credentials: 'include',
});
if (!response.ok) {
console.error('Falha ao obter identidade do widget');
return;
}
const { user_id, identifier_hash, name, email } = await response.json();
window.$cloudchat.setUser(user_id, {
identifier_hash: identifier_hash,
name: name,
email: email,
});
} catch (error) {
console.error('Erro ao configurar usuário do widget:', error);
}
});
</script>
:::success Vantagens:
- O hmac_token nunca sai do servidor
- O identifier_hash nunca é persistido no navegador
- Cada aba obtém dados frescos, garantindo consistência
- Logout retorna 401, e o widget não é identificado
:::
Opção alternativa: localStorage (com ressalvas)
Use somente se sua aplicação não tem backend disponível para fornecer o identifier_hash (SPAs puramente estáticas).
:::warning Aviso de segurança: o localStorage é acessível por qualquer script JavaScript no mesmo domínio. Se o site for
vulnerável a XSS, um atacante pode ler o identifier_hash armazenado.
:::
Aba que gera os dados (ex: página de login)
const userData = {
user_id: '12345',
identifier_hash: 'abc123def456...',
name: 'Maria Silva',
email: '[email protected]',
};
localStorage.setItem('cloudchat_user', JSON.stringify(userData));
Qualquer aba que carrega o widget
window.addEventListener('cloudchat:ready', function () {
const stored = localStorage.getItem('cloudchat_user');
if (!stored) return;
try {
const { user_id, identifier_hash, name, email } = JSON.parse(stored);
window.$cloudchat.setUser(user_id, {
identifier_hash,
name,
email,
});
} catch (e) {
console.error('Dados do widget inválidos no localStorage');
localStorage.removeItem('cloudchat_user');
}
});
Ao fazer logout
localStorage.removeItem('cloudchat_user');
window.$cloudchat.reset();
Opção avançada: sessionStorage + BroadcastChannel
Combina a não persistência do sessionStorage com 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 = 'cloudchat-session';
const STORAGE_KEY = 'cloudchat_user';
const channel = new BroadcastChannel(CHANNEL_NAME);
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') {
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.$cloudchat.reset();
}
};
function initWidget(userData) {
window.$cloudchat.setUser(userData.user_id, {
identifier_hash: userData.identifier_hash,
name: userData.name,
email: userData.email,
});
}
window.addEventListener('cloudchat:ready', function () {
const stored = sessionStorage.getItem(STORAGE_KEY);
if (stored) {
initWidget(JSON.parse(stored));
return;
}
channel.postMessage({ type: 'session-request' });
// Timeout: se nenhuma aba responder em 2s, esta é a primeira aba
});
Ao autenticar (aba principal)
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.$cloudchat.reset();
:::success 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
:::
:::warning Limitações:
- BroadcastChannel não é suportado no IE11 (mas funciona em todos os navegadores modernos)
- Se o usuário fechar todas as abas, precisa autenticar novamente
:::
Segurança
O que NUNCA fazer
:::error Nunca exponha o hmac_token (chave secreta) no frontend. É a chave que o servidor usa para gerar o
identifier_hash. Se um atacante a obtém, pode personificar qualquer usuário.
// ❌ ERRADO
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:
- É uma assinatura (HMAC) do user_id, não um token de acesso
- Um atacante precisa do user_id correspondente E da validação HMAC ativa para personificar alguém
- Não concede acesso a sistemas além do widget de chat
- O impacto de vazamento é limitado: o atacante poderia, no máximo, abrir conversas no widget como aquele usuário
Ainda assim, a opção server-side é preferível por eliminar a exposição.
Tabela comparativa de armazenamento
Boas práticas
:::success
1. Proteja-se contra XSS — é a ameaça nº 1 para qualquer dado no navegador. Use Content Security Policy (CSP), sanitize
inputs, 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 — carregue o widget apenas em páginas necessárias
5. Limpe dados ao fazer logout — remova localStorage/sessionStorage e chame $cloudchat.reset()
:::
Referências:
- OWASP HTML5 Security Cheat Sheet
- Auth0 — Secure Browser Storage
Reset de sessão
Use window.$cloudchat.reset() para limpar a sessão. Internamente:
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 sessão limpa
Quando usar
Exemplo: logout completo
function onLogout() {
// 1. Limpar dados armazenados (se localStorage)
localStorage.removeItem('cloudchat_user');
// 2. Resetar o widget
if (window.$cloudchat) {
window.$cloudchat.reset();
}
// 3. Redirecionar para página de login
window.location.href = '/login';
}
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 se há erros de rede (HTTP 401 no endpoint /widget/contact)
2. Confirme que o identifier_hash foi gerado com o hmac_token correto
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. Endpoint server-side: confirme que ambas as abas chamam o mesmo endpoint e usuário logado
2. localStorage: verifique se os dados foram gravados antes da segunda aba ler
Problema: Após logout, widget ainda mostra dados do usuário anterior
Causa provável: $cloudchat.reset() não foi chamado, ou os cookies não foram limpos.
Solução:
1. Chame window.$cloudchat.reset() antes ou durante o 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á como undefined, null ou objeto.
Solução: garanta que é string ou número:
window.$cloudchat.setUser(String(userData.user_id), { ... });
Problema: setUser() lança erro "User object should have one of the keys..."
Causa: o objeto não contém nenhuma das chaves obrigatórias.
Solução: inclua ao menos uma de name, email ou avatar_url:
window.$cloudchat.setUser(userId, {
identifier_hash: hash,
name: 'Nome do Usuário',
});
Problema: BroadcastChannel não sincroniza entre abas
Causa provável: abas 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). Garanta que todas as abas usam
exatamente a mesma URL base.
Observações
- Para a instalação básica do Web Widget: Como instalar o Web Widget do Cloud Chat no site
- Para múltiplas conversas e histórico (que essa feature pressupõe): Como ativar múltiplas conversas e histórico no
Web Widget
- Para uso em apps mobile via WebView: Como integrar o Web Widget em aplicativos mobile