c.l.cladDocs

Identity verification (JWT)

Securely identify logged-in users with short-lived, server-signed JWTs. Includes backend recipes.

Authenticated users must be verified with a short‑lived JWT minted by your backend — never put secrets in the browser. The SDK calls your tokenProvider on boot, before expiry, and after a 401.

1. Get your widget's signing secret

In Settings → Integrations → Web Widget, copy the JWT secret. Store it as a backend environment variable (e.g. SUPPORT_CHAT_WIDGET_SECRET). Treat it like a password; rotate it from the same screen if leaked.

2. Expose a token endpoint on your backend

The endpoint returns a signed JWT (HS256) for the currently logged‑in user.

Required claims: sub (your stable user id), widget_id, workspace_id, and exp. Recommended: email, name, company_id, jti.

{
  "iss": "your_backend",
  "aud": "clad_support_chat",
  "sub": "user_123",
  "email": "rachel@example.com",
  "name": "Rachel",
  "company_id": "org_456",
  "workspace_id": "{YOUR_WORKSPACE_ID}",
  "widget_id": "{YOUR_WIDGET_ID}",
  "iat": 1760000000,
  "exp": 1760000900,
  "jti": "a-unique-token-id"
}
Node.js (Express)
import jwt from "jsonwebtoken";
 
app.get("/api/support-chat-token", requireLogin, (req, res) => {
  const token = jwt.sign(
    {
      sub: req.user.id,
      email: req.user.email,
      name: req.user.name,
      company_id: req.user.orgId,
      workspace_id: "{YOUR_WORKSPACE_ID}",
      widget_id: "{YOUR_WIDGET_ID}",
    },
    process.env.SUPPORT_CHAT_WIDGET_SECRET,
    { expiresIn: "15m", audience: "clad_support_chat" }
  );
  res.type("text/plain").send(token);
});
Python (Flask)
import jwt, time, os
from flask import Response
 
@app.get("/api/support-chat-token")
@login_required
def support_chat_token():
    now = int(time.time())
    token = jwt.encode({
        "sub": current_user.id,
        "email": current_user.email,
        "name": current_user.name,
        "company_id": current_user.org_id,
        "workspace_id": "{YOUR_WORKSPACE_ID}",
        "widget_id": "{YOUR_WIDGET_ID}",
        "iat": now, "exp": now + 900,
        "aud": "clad_support_chat",
    }, os.environ["SUPPORT_CHAT_WIDGET_SECRET"], algorithm="HS256")
    return Response(token, mimetype="text/plain")
Ruby (Rails)
def support_chat_token
  payload = {
    sub: current_user.id, email: current_user.email, name: current_user.name,
    company_id: current_user.org_id, workspace_id: "{YOUR_WORKSPACE_ID}", widget_id: "{YOUR_WIDGET_ID}",
    iat: Time.now.to_i, exp: 15.minutes.from_now.to_i, aud: "clad_support_chat"
  }
  render plain: JWT.encode(payload, ENV["SUPPORT_CHAT_WIDGET_SECRET"], "HS256")
end
Go
claims := jwt.MapClaims{
    "sub": user.ID, "email": user.Email, "name": user.Name,
    "company_id": user.OrgID, "workspace_id": "{YOUR_WORKSPACE_ID}", "widget_id": "{YOUR_WIDGET_ID}",
    "iat": time.Now().Unix(), "exp": time.Now().Add(15 * time.Minute).Unix(),
    "aud": "clad_support_chat",
}
tok, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
    SignedString([]byte(os.Getenv("SUPPORT_CHAT_WIDGET_SECRET")))
w.Write([]byte(tok))

3. Wire it into the SDK

await chat.boot({
  user: { id: user.id, email: user.email, name: user.name },
  tokenProvider: () => fetch("/api/support-chat-token").then((r) => r.text()),
});

Token lifecycle

  • The SDK requests a token on boot, refreshes it before expiry, and again after any 401.
  • Keep tokens short‑lived (≤ 15 minutes). The widget handles refresh transparently; expiry surfaces as an auth:expired event.
  • The platform verifies signature, expiration, audience, and that workspace_id / widget_id match the widget — and that the request origin is allow‑listed.