Home Channels and inboxes Shared Web Widget Session Across Browser Tabs

Shared Web Widget Session Across Browser Tabs

Last updated on Apr 13, 2026

1. Introduction

This guide explains how to maintain the same user session of the Cloud Chat web widget across two or more browser tabs without requiring each tab to be authenticated individually in the host application.

Typical scenario: a user opens your website in two tabs and starts a conversation via the widget in one tab. In the second tab, the widget should recognize the same user and display the same conversation history.

When to use this guide

  • Your application allows multiple tabs to be open simultaneously.

  • You already use (or want to use) the widget's Identity Validation.

  • You want the widget to identify the same contact across all tabs without requiring login in each.


2. Prerequisites


3. How it works (simplified explanation)

Think of it like this:

  1. user_id is the "name on the badge" — identifies who the user is.

  2. identifier_hash is the "authentication stamp" — proves the badge is genuine.

  3. Cloud Chat verifies the stamp (using a secret key known only to the server) before granting access to the history.

Tab-to-Tab Flow

Key point: each tab must call setUser() independently. The cw_user_* cookie is shared automatically between tabs (same domain), but the authToken within the widget is per instance (per tab). Therefore, both tabs need access to user_id and identifier_hash to initialize correctly.


4. Implementation

4.1 Recommended option: Server-side endpoint

This is the most secure option. The identifier_hash is never stored in the browser — it is obtained on demand from a protected endpoint authenticated with your app.

Backend (Node.js/Express example)

const crypto = require('crypto');

// Endpoint protected by your app’s authentication

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

  const userId = String(req.user.id); // Logged-in user ID

  const hmacToken = process.env.CLOUDCHAT_HMAC_TOKEN; // Widget secret key

  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 example)

# 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 (Python/Django example)

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 (any tab)

<script>

  window.CloudChatSettings = {

    // ... widget configuration

  };

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

    try {

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

        credentials: 'include', // Send session cookies

      });

      if (!response.ok) {

        console.error('Failed to obtain widget identity');

        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 configuring widget user:', error);

    }

  });

</script>

Advantages:

  • The hmac_token never leaves the server.

  • The identifier_hash is never stored in the browser.

  • Each tab gets fresh data, maintaining consistency.

  • If the user logs out, the endpoint returns 401, and the widget is not identified.


4.2 Alternative: localStorage (with caveats)

Use this only if your app does not have a backend available to provide identifier_hash (e.g., static SPAs).

Security Notice

localStorage is accessible by any JavaScript running on the same domain. If your site is vulnerable to XSS (Cross-Site Scripting), an attacker could read the stored identifier_hash. See section 5 (Security) for risks.

Tab that generates data (e.g., login page)

// After authentication, your backend returns user_id and identifier_hash

const userData = {

  user_id: '12345',

  identifier_hash: 'abc123def456...', // Generated by your backend

  name: 'Maria Silva',

  email: '[email protected]',

};

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

Any tab that loads the 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('Invalid widget data in localStorage');

    localStorage.removeItem('CloudChat_user');

  }

});

On logout

localStorage.removeItem('CloudChat_user');

window.$CloudChat.reset();

4.3 Advanced option: sessionStorage + BroadcastChannel

This combines the non-persistence of sessionStorage with real-time synchronization between tabs via the BroadcastChannel API. Data exists only while at least one tab is open.

Initialization (in all tabs)

const CHANNEL_NAME = 'CloudChat-session';
const STORAGE_KEY = 'CloudChat_user';

const channel = new BroadcastChannel(CHANNEL_NAME);

// When receiving data from another tab

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

    // Another tab requests data — send if available

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

  // Check if data already exists in this tab

  const stored = sessionStorage.getItem(STORAGE_KEY);

  if (stored) {

    initWidget(JSON.parse(stored));

    return;

  }

  // Request data from other tabs

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

  // Timeout: if no response in 2 seconds, this becomes the first tab
  // (user will need to log in normally)

});

On authentication (main tab)

// After receiving user_id and identifier_hash from 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);

On logout

sessionStorage.removeItem(STORAGE_KEY);

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

window.$CloudChat.reset();

Advantages:

  • Data does not persist after closing all tabs.

  • Real-time synchronization between tabs.

  • No dependency on JavaScript-accessible cookies.

Limitations:

  • BroadcastChannel is not supported in IE11 (but works in all modern browsers).

  • If all tabs are closed and reopened, user must log in again.


5. Security

What NEVER to do

Never expose the hmac_token (secret key) in the frontend. This key is used by your server to generate the identifier_hash. If an attacker obtains the hmac_token, they can generate valid hashes for any user_id, impersonating any user.

// WRONG — NEVER do this

const hmacToken = 'your-secret-key-here';

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

Risks of storing identifier_hash in localStorage

Context of the risk:

The identifier_hash itself is not a high-value secret:

  • It is a signature (HMAC) of the user_id, not an access token.

  • An attacker needs the corresponding user_id AND the HMAC validation to impersonate someone.

  • identifier_hash does not grant access to systems beyond the chat widget.

  • The impact of a leak is limited: at worst, an attacker could initiate conversations as that user within the widget.

However, the server-side endpoint option is preferred to eliminate exposure entirely.

Storage approach comparison table

Best Practices

  1. Protect against XSS — the number one threat to any data in the browser. Use Content Security Policy (CSP), sanitize inputs, and keep dependencies up to date.

  2. Generate identifier_hash on the server — never in the frontend.

  3. Use HTTPS — always. No exceptions.

  4. Limit attack surface — if possible, load the widget only on pages where it’s needed.

  5. Clear data on logout — remove data from localStorage/sessionStorage and call $CloudChat.reset().

References:


6. Session Reset

Use window.$CloudChat.reset() to clear the widget session. Internally, reset performs:

  1. Closes the chat if open.

  2. Removes the cw_conversation cookie (session token).

  3. Removes the cw_user_{websiteToken} cookie (user identity).

  4. Reloads the widget iframe, starting a clean session.

When to use

Example: full logout


function onLogout() {

  // 1. Clear stored data (if using localStorage)

  localStorage.removeItem('CloudChat_user');

  // 2. Reset the widget

  if (window.$CloudChat) {

    window.$CloudChat.reset();

  }

  // 3. Redirect to login page

  window.location.href = '/login';

}

7. Troubleshooting

Issue: Widget shows "Start new conversation" instead of history

Likely cause: setUser() was not called, or called with an invalid identifier_hash.

Solution:

  1. Check in browser console for network errors (HTTP 401 at /widget/contact).

  2. Confirm that the identifier_hash was generated with the correct hmac_token (from widget panel).

  3. Make sure the user_id passed to setUser() matches the one used to generate the hash.

Issue: Different tabs show different users

Likely cause: each tab is calling setUser() with different data.

Solution:

  1. If using server endpoint, ensure both tabs call the same endpoint for the same logged-in user.

  2. If using localStorage, ensure data was written before the second tab tries to read.

Issue: After logout, widget still shows previous user’s data

Likely cause: $CloudChat.reset() was not called, or cookies were not cleared.

Solution:

  1. Call $CloudChat.reset() before or during logout.

  2. Clear localStorage/sessionStorage if storing data there.

  3. Verify that the cw_user_* cookie was removed (DevTools > Application > Cookies).

Issue: setUser() throws "Identifier should be a string or number" error

Cause: user_id is undefined, null, or an object.

Solution: ensure value is a string or number. If from API, convert:

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

Issue: setUser() throws "User object should have one of the keys..." error

Cause: user object lacks required keys.

Solution: include at least one of: name, email, or avatar_url.

// Example
window.$CloudChat.setUser(userId, {
  identifier_hash: hash,
  name: 'User Name', // At least one key required
});

Issue: BroadcastChannel does not sync across tabs

Likely cause: tabs are on different domains (e.g., app.example.com vs www.example.com).

Solution: BroadcastChannel only works between pages on the same origin. Make sure all tabs access exactly the same URL domain.