Identity tokens

An identity token tells KauZap which of your users is chatting. Your server signs it with a secret only you and KauZap know, so a visitor cannot pretend to be someone else.

Format

A JWT signed with HS256. The header carries kid, the id of the identity secret you signed with.

ClaimRequiredMeaning
issYesThe property's public key, pk_…
subYesYour stable user id. The same sub always maps to the same contact.
iatYesIssued at, Unix seconds
expYesExpiry, Unix seconds, at most 600 seconds after iat
jtiYesA unique id. Each token can be used once.
name, email, phoneNoShown to agents and kept on the contact
attributesNoAn object of your own fields, such as plan or account id, shown in the customer panel

Mint one

import { createHmac, randomUUID } from 'node:crypto';

const b64url = (value) => Buffer.from(value).toString('base64url');

/**
 * Mint a KauZap identity token (JWT, HS256) on your server. Never do this in the browser:
 * the secret must stay on your backend.
 *
 * @param {object} opts
 * @param {string} opts.secret Identity secret for the property (Settings, Developers).
 * @param {string} opts.kid Key id of that secret, sent in the JWT header.
 * @param {string} opts.propertyKey The property's public key (pk_...), sent as `iss`.
 * @param {string} opts.sub Your stable user id.
 * @param {string} [opts.name]
 * @param {string} [opts.email]
 * @param {string} [opts.phone]
 * @param {Record<string, unknown>} [opts.attributes] Shown to agents in the customer panel.
 * @param {number} [opts.ttlSeconds] Lifetime, at most 600 seconds. Default 300.
 * @param {number} [opts.now] Current time in milliseconds. Default Date.now().
 * @returns {string}
 */
export function mintIdentityToken({
  secret,
  kid,
  propertyKey,
  sub,
  name,
  email,
  phone,
  attributes,
  ttlSeconds = 300,
  now = Date.now(),
}) {
  if (!secret || !kid || !propertyKey || !sub) {
    throw new Error('secret, kid, propertyKey and sub are required');
  }
  if (!(ttlSeconds > 0 && ttlSeconds <= 600)) {
    throw new Error('ttlSeconds must be between 1 and 600');
  }
  const iat = Math.floor(now / 1000);
  // JSON.stringify drops the optional claims you left undefined.
  const claims = {
    iss: propertyKey,
    sub,
    name,
    email,
    phone,
    attributes,
    iat,
    exp: iat + ttlSeconds,
    jti: randomUUID(),
  };
  const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid }));
  const input = `${header}.${b64url(JSON.stringify(claims))}`;
  const signature = createHmac('sha256', secret).update(input).digest('base64url');
  return `${input}.${signature}`;
}
examples/node-integration/identity-token.mjs

Any JWT library works too: sign with HS256, put kid in the header and keep the lifetime at 10 minutes or less.

What KauZap checks

  1. The signature, using the secret named by kid, compared in constant time.
  2. iss matches the property the widget runs for.
  3. The lifetime is at most 10 minutes, and iat and exp are within 60 seconds of our clock.
  4. The page's origin is in the property's allowed origins.
  5. The jti has not been used before. A replayed token is rejected.
Error codeWhat happenedWhat to do
invalid_identity_tokenBad signature, unknown kid, wrong iss, expired, or lifetime over 10 minutesCheck the secret, kid, property key and your server clock
identity_token_replayedThe jti was already usedMint a new token for every identify call
origin_not_allowedThe page origin is not in the allowed originsAdd the origin to the property

Rotate a secret

  1. Create a second identity secret for the property. It gets a new kid.
  2. Deploy your server with the new secret and kid. Tokens signed with either secret are accepted meanwhile.
  3. Revoke the old secret once no server signs with it.

Anonymous visitors

Visitors can chat before they sign in. When you later call KauZap.identify with a valid token, the anonymous conversation is merged into the identified contact.

Never mint tokens in the browser and never ship the identity secret in front-end code. Anyone holding it can sign in as any of your users.