Sessão compartilhada do Web Widget entre abas do navegador
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:
- OWASP HTML5 Security Cheat Sheet
- Auth0 — Secure Browser Storage
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.