Inicio Canales y buzones de entrada Sesión compartida del Web Widget entre pestañas del navegador

Sesión compartida del Web Widget entre pestañas del navegador

Última actualización el Apr 13, 2026

1. Introducción

Esta guía explica cómo mantener la misma sesión de usuario del widget web Cloud Chat en dos o más pestañas del navegador, sin requerir que cada pestaña esté autenticada en la aplicación host.

Escenario típico: el usuario abre su sitio en varias pestañas y inicia una conversación a través del widget en una de ellas. En otra pestaña, el widget debe reconocer al mismo usuario y mostrar el mismo historial de conversaciones.

Cuándo usar esta guía

  • Tu aplicación permite múltiples pestañas abiertas simultáneamente.

  • Ya utilizas (o deseas utilizar) la Validación de Identidad del widget.

  • Quieres que el widget identifique al mismo contacto en todas las pestañas, sin requerir login en cada una.


2. Requisitos previos


3. Cómo funciona (explicación simplificada)

Piensa así:

  1. user_id es el "nombre en la placa" — identifica quién es el usuario.

  2. identifier_hash es el "sello de autenticidad" — prueba de que la placa es verdadera.

  3. Cloud Chat verifica el sello (usando una llave secreta que solo el servidor conoce) antes de liberar el acceso al historial.

Flujo entre pestañas

Punto clave: cada pestaña necesita llamar a setUser() independientemente. La cookie cw_user_* se comparte automáticamente entre pestañas (mismo dominio), pero el authToken interno del widget es por instancia (por pestaña). Por eso, ambas pestañas necesitan tener acceso al user_id y al identifier_hash para inicializar correctamente.


4. Implementación

4.1 Opción recomendada: Endpoint del lado del servidor

Esta es la opción más segura. El identifier_hash nunca se almacena en el navegador — se obtiene bajo demanda desde un endpoint protegido por la autenticación de tu aplicación.

Backend (ejemplo Node.js/Express)

const crypto = require('crypto');

// Ruta protegida por autenticación de tu aplicación

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

  const userId = String(req.user.id); // ID del usuario logueado

  const hmacToken = process.env.CLOUDCHAT_HMAC_TOKEN; // Llave secreta del 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 (ejemplo 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 (ejemplo 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 (cualquier pestaña)

<script>

  window.CloudChatSettings = {

    // ... configuraciones del widget

  };

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

    try {

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

        credentials: 'include', // Envía cookies de sesión

      });

      if (!response.ok) {

        console.error('Fallo al obtener la identidad del 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('Error al configurar el usuario del widget:', error);

    }

  });

</script>

Ventajas:

  • El hmac_token nunca sale del servidor.

  • El identifier_hash nunca se persiste en el navegador.

  • Cada pestaña obtiene datos frescos, asegurando consistencia.

  • Si el usuario cierra sesión, el endpoint devuelve 401 y el widget no se identifica.


4.2 Opción alternativa: localStorage (con precaución)

Usa esta opción solo si tu aplicación no tiene un backend disponible para proporcionar el identifier_hash (por ejemplo, SPAs puramente estáticas).

Aviso de Seguridad

El localStorage es accesible por cualquier script JavaScript que se ejecute en el mismo dominio. Si tu sitio es vulnerable a XSS (Cross-Site Scripting), un atacante puede leer el identifier_hash almacenado. Consulta la sección 5 (Seguridad) para entender los riesgos.

Pestaña que genera los datos (ej: página de login)

// Tras autenticación, tu backend devuelve user_id y identifier_hash

const userData = {

  user_id: '12345',

  identifier_hash: 'abc123def456...', // Generado por tu backend

  name: 'Maria Silva',

  email: '[email protected]',

};

localStorage.setItem('CloudChat_user', JSON.stringify(userData));

Cualquier pestaña que carga el 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: identifier_hash,

      name: name,

      email: email,

    });

  } catch (e) {

    console.error('Datos inválidos del widget en localStorage');

    localStorage.removeItem('CloudChat_user');

  }

});

Al cerrar sesión

localStorage.removeItem('CloudChat_user');

window.$CloudChat.reset();

4.3 Opción avanzada: sessionStorage + BroadcastChannel

Esta opción combina la no persistencia del sessionStorage con la sincronización entre pestañas vía BroadcastChannel API. Los datos solo existen mientras al menos una pestaña esté abierta.

Inicialización (en todas las pestañas)

const CHANNEL_NAME = 'CloudChat-session';

const STORAGE_KEY = 'CloudChat_user';

const channel = new BroadcastChannel(CHANNEL_NAME);

// Al recibir datos de otra pestaña

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') {

    // Otra pestaña pidió los datos — enviar si los tenemos

    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 () {

  // Verificar si ya tenemos datos en esta pestaña

  const stored = sessionStorage.getItem(STORAGE_KEY);

  if (stored) {

    initWidget(JSON.parse(stored));

    return;

  }

  // Pedir datos a otras pestañas

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

  // Tiempo de espera: si ninguna pestaña responde en 2s, esta es la primera
  // (el usuario deberá iniciar sesión normalmente)

});

Al autenticar (pestaña principal)

// Tras recibir user_id y identifier_hash del 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);

Al cerrar sesión

sessionStorage.removeItem(STORAGE_KEY);

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

window.$CloudChat.reset();

Ventajas:

  • Los datos no persisten tras cerrar todas las pestañas.

  • Sincronización en tiempo real entre pestañas.

  • Sin dependencia de cookies accesibles por JS.

Limitaciones:

  • BroadcastChannel no es compatible con IE11 (pero funciona en todos los navegadores modernos).

  • Si el usuario cierra todas las pestañas y las vuelve a abrir, deberá autenticarse nuevamente.


5. Seguridad

Qué NUNCA hacer

Nunca expongas el hmac_token (llave secreta) en el frontend. Esta llave la usa el servidor para generar el identifier_hash. Si un atacante obtiene el hmac_token, puede generar hashes válidos para cualquier user_id, personificando a cualquier usuario.

// ERRÓNEO — NUNCA hagas esto

const hmacToken = 'tu-llave-secreta-aqui';

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

Riesgos de almacenar identifier_hash en localStorage

Contextualización del riesgo

El identifier_hash por sí solo no es un secreto de alto valor:

  • Es una firma (HMAC) del user_id, no un token de acceso.

  • Un atacante necesita el user_id correspondiente Y que la validación HMAC esté activa para personificar a alguien.

  • El identifier_hash no concede acceso a sistemas fuera del widget de chat.

  • El impacto de una filtración es limitado: el atacante podría, como mucho, abrir conversaciones como ese usuario en el widget.

Aun así, la opción del endpoint del lado del servidor es preferible porque elimina completamente la exposición.

Tabla comparativa de enfoques de almacenamiento

Buenas prácticas

  1. Protégete contra XSS — es la amenaza número 1 para cualquier dato en el navegador. Usa Content Security Policy (CSP), sanea las entradas y mantiene actualizadas las dependencias.

  2. Genera identifier_hash en el servidor — nunca en el frontend.

  3. Usa HTTPS — siempre. Sin excepciones.

  4. Limita la superficie de ataque — si es posible, carga el widget solo en las páginas donde sea necesario.

  5. Limpia los datos al cerrar sesión — elimina localStorage/sessionStorage y llama a $CloudChat.reset().

Referencias:


6. Reseteo de sesión

Usa window.$CloudChat.reset() para limpiar la sesión del widget. Internamente, el reset:

  1. Cierra el chat si está abierto.

  2. Elimina la cookie cw_conversation (token de sesión de la conversación).

  3. Elimina la cookie cw_user_{websiteToken} (identidad del usuario).

  4. Recarga el iframe del widget, iniciando una sesión limpia.

Cuándo usar

Ejemplo: cierre de sesión completo

function onLogout() {

  // 1. Limpiar datos almacenados (si usas localStorage)

  localStorage.removeItem('CloudChat_user');

  // 2. Resetear el widget

  if (window.$CloudChat) {

    window.$CloudChat.reset();

  }

  // 3. Redirigir a la página de login

  window.location.href = '/login';
}

7. Resolución de problemas

Problema: El widget muestra "Iniciar nueva conversación" en lugar del historial

Causa probable: setUser() no fue llamado, o se llamó con identifier_hash inválido.

Solución:

  1. Verifica en la consola del navegador si hay errores de red (HTTP 401 en el endpoint /widget/contact).

  2. Confirma que el identifier_hash fue generado con el hmac_token correcto (desde el panel del widget).

  3. Confirma que el user_id pasado a setUser() sea el mismo que usaste para generar el hash.

Problema: las pestañas muestran usuarios diferentes

Causa probable: cada pestaña llama a setUser() con datos distintos.

Solución:

  1. Si utilizas endpoint del lado del servidor, confirma que ambas llamadas sean al mismo endpoint y que el usuario logueado sea el mismo.

  2. Si usas localStorage, verifica que los datos se hayan almacenado antes de que la segunda pestaña intente leer.

Problema: después de cerrar sesión, el widget aún muestra datos del usuario anterior

Causa probable: no se llamó a $CloudChat.reset(), o los cookies no se borraron.

Solución:

  1. Llama a $CloudChat.reset() antes o durante el proceso de logout.

  2. Limpia localStorage/sessionStorage si guardas datos allí.

  3. Verifica que la cookie cw_user_* haya sido eliminada (DevTools > Aplicación > Cookies).

Problema: setUser() lanza error "Identifier should be a string or a number"

Causa: user_id se pasa como undefined, null o como objeto.

Solución: asegúrate de que el valor sea una cadena o número. Si proviene de una API, haz la conversión:

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

Problema: setUser() lanza error "User object should have one of the keys..."

Causa: el objeto de usuario no contiene alguna de las claves obligatorias.

Solución: incluye al menos una de estas claves: name, email o avatar_url.

window.$CloudChat.setUser(userId, {

  identifier_hash: hash,

  name: 'Nombre del usuario', // Obligatorio tener al menos uno: name, email o avatar_url

});

Problema: BroadcastChannel no sincroniza entre pestañas

Causa probable: las pestañas están en diferentes dominios (ej: app.ejemplo.com vs www.ejemplo.com).

Solución: BroadcastChannel solo funciona entre páginas del mismo origen (same-origin). Asegúrate de que todas las pestañas usen exactamente la misma URL base.