> For the complete documentation index, see [llms.txt](https://docs.klink.cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.klink.cloud/getting-started/setup-channels/live-chat/identity-verification-secure-user-login.md).

# Identity Verification (Secure User Login)

Identity verification proves that the `authUser` passed to the K-Link chat widget really is the person your website says it is. Without it, anyone who opens the widget could claim to be another user and read that user's conversation history.

There are two ways to do it:

| Method                        | How it works                                                                                                                                         | Status                               |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| **`userToken`** (recommended) | Your server signs a **short-lived token** (JWT). A leaked token stops working within minutes, and the user's name, email and phone are protected too | Recommended for all new integrations |
| `userHash`                    | Your server computes an HMAC of the user's identity. It **never expires**, and only the identity is protected                                        | Still supported                      |

Both use the **same channel secret** (step 1).

### Choosing the identity value

The identity is whatever stable, unique value identifies the user in your system — a customer id, email, phone number, or any other unique id. Pass it in the `identity` field. If you don't set `identity`, the widget uses `email` as the identity, so existing email-based integrations keep working unchanged.

**The one rule that matters:** the `userHash` must sign the exact value that serves as the identity — i.e. `identity` if you send it, otherwise `email`. That same value is what the backend uses to recognize a returning user, so pick one identity strategy per channel and stay with it. `email`/`phone` you also pass are stored as profile attributes for display; only the identity value is signed and deduplicated on.

### 1. Generate the channel's identity secret

Use the K-Link public API with your existing API token (`kpt_…`):

```bash
curl -X POST "https://apigw.klinkcx.com/api/v1/webchat/channels/<channelId>/identity-verification/secret" \
  -H "Authorization: Bearer kpt_xxxxxxxx"
# => { "requestId": "…", "secret": "9f2c4a…64 hex chars…" }
```

`<channelId>` is your klink.cloud webchat channel id — the same value passed to the widget as `scId`. The API only accepts channels that belong to your tenant.

The plaintext secret is returned only by this call. Store it in your server's secret manager — never in frontend code, HTML, or public repos. Calling the endpoint again rotates the secret (the old one stops working immediately).

### 2. Compute `userHash` on your server (legacy method)

{% hint style="info" %}
For new integrations, use an expiring [`userToken`](#recommended-expiring-usertoken) instead of steps 2 and 3.
{% endhint %}

Hash the **exact identity string** you pass to the widget — the value of `identity` (or `email` if you don't send `identity`). The comparison is byte-for-byte, so watch out for casing/whitespace differences. In the examples below `identity` is the customer id you pass as `authUser.identity`.

Node.js:

```js
const crypto = require("crypto");

const userHash = crypto
  .createHmac("sha256", process.env.KLINK_IDENTITY_SECRET)
  .update(identity) // e.g. user.id — or user.email if that's your identity
  .digest("hex");
```

PHP:

```php
$userHash = hash_hmac('sha256', $identity, getenv('KLINK_IDENTITY_SECRET'));
```

Python:

```python
import hashlib, hmac, os

user_hash = hmac.new(
    os.environ["KLINK_IDENTITY_SECRET"].encode(),
    identity.encode(),
    hashlib.sha256,
).hexdigest()
```

### 3. Pass it to the widget

Render the hash into the page for the logged-in user. Identify by any stable unique value via `identity`:

```html
<script>
  klinkChatSDK.run({
    serverUrl: "https://platform.klinkcx.com",
    scId: "<your-channel-id>",
    authUser: {
      name: "Jane Doe",
      identity: "cust_9931", // your stable user id; the value the hash signs
      email: "jane@example.com", // optional profile attribute
      phone: "+95912345678", // optional profile attribute
      userHash: "<hmac-of-identity-computed-on-your-server>",
    },
  });
</script>
```

If you identify users by email, omit `identity` and sign the email instead — the existing behavior:

```html
<script>
  klinkChatSDK.run({
    serverUrl: "https://platform.klinkcx.com",
    scId: "<your-channel-id>",
    authUser: {
      name: "Jane Doe",
      email: "jane@example.com", // used as the identity; sign this value
      userHash: "<hmac-of-email-computed-on-your-server>",
    },
  });
</script>
```

Anonymous visitors (no login) simply omit `authUser` — nothing changes for them.

### Recommended: Expiring `userToken`

A `userToken` is a standard **JWT** signed with **HS256** using your channel secret from step 1. Create it on your server each time the user opens the chat.

| Field                    | Required    | Description                                                                                    |
| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------- |
| `sub`                    | ✅           | Your user's unique, **stable** ID (e.g. `"cust_9931"`). The same rules apply as for `identity` |
| `exp`                    | ✅           | Expiry time (unix seconds). **Recommended: 10 minutes from now.** Maximum 24 hours             |
| `iat`                    | Recommended | Issue time (unix seconds)                                                                      |
| `name`, `email`, `phone` | Optional    | Shown to agents. These are signed, so visitors can't change them                               |

**Node.js** (`npm install jsonwebtoken`)

```js
const jwt = require("jsonwebtoken");

const userToken = jwt.sign(
  { sub: user.id, name: user.name, email: user.email },
  process.env.KLINK_IDENTITY_SECRET,
  { algorithm: "HS256", expiresIn: "10m" },
);
```

**Python** (`pip install pyjwt`)

```python
import jwt, os, time

now = int(time.time())
user_token = jwt.encode(
    {"sub": user.id, "name": user.name, "email": user.email, "iat": now, "exp": now + 600},
    os.environ["KLINK_IDENTITY_SECRET"],
    algorithm="HS256",
)
```

**PHP** (`composer require firebase/php-jwt`)

```php
$now = time();
$userToken = \Firebase\JWT\JWT::encode(
    ['sub' => $user->id, 'name' => $user->name, 'iat' => $now, 'exp' => $now + 600],
    getenv('KLINK_IDENTITY_SECRET'),
    'HS256'
);
```

Pass it to the widget instead of `identity` + `userHash`:

```html
<script>
  klinkChatSDK.run({
    serverUrl: "https://apigw.klink.cloud",
    scId: "<your-channel-id>",
    authUser: { userToken: "<token from your server>" },
  });
</script>
```

For the mobile app / hosted chat page, add it to the URL: `…/app/index.html?scId=<your-channel-id>&userToken=<token>`.

{% hint style="info" %}
**The chat stays connected after the token expires.** Once the chat has opened, K-Link keeps the user signed in for their session, including through network drops. Create a new token each time the chat is **opened**. You don't need to refresh it while the chat stays open.
{% endhint %}

* If you identify users by email, set `sub` to the email **and** include the `email` field. Returning users keep their conversation history.
* Only `HS256` is accepted. Up to 60 seconds of clock difference between your server and K-Link is tolerated.

#### Turn off `userHash` for your channel

Once **all** your pages and app versions send `userToken`, stop accepting the non-expiring `userHash`:

```bash
curl -X PUT "https://apigw.klinkcx.com/api/v1/webchat/channels/<channelId>/identity-verification" \
  -H "Authorization: Bearer kpt_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "requireToken": true }'
```

{% hint style="warning" %}
Older versions of your mobile app stay installed on users' phones. Turn on `requireToken` only once no supported app version still uses `userHash`, or those users will chat as guests.
{% endhint %}

#### Troubleshooting `userToken`

If a token is rejected, the user continues as a guest and the browser console shows a message starting with `[KLinkChat]`:

| Message                                        | Fix                                                                                  |
| ---------------------------------------------- | ------------------------------------------------------------------------------------ |
| `userToken has expired…`                       | Create a new token every time the chat opens; don't cache pages that contain a token |
| `userToken signature is invalid…`              | Sign with the channel's **current** secret                                           |
| `userToken is not a valid HS256 JWT…`          | Use the HS256 algorithm                                                              |
| `userToken must contain sub … and exp …`       | Add both fields                                                                      |
| `userToken is valid for too long…`             | Use an expiry of at most 24 hours (10 minutes recommended)                           |
| `userToken is not valid yet…`                  | Sync your server clock (NTP)                                                         |
| `This channel requires an expiring userToken…` | This page or app version still sends `userHash`; update it to send `userToken`       |

You can also check verification in code:

```javascript
klinkChatSDK.run({ /* … */ onReady: ({ identityVerified }) => console.log(identityVerified) });
```

### 4. Enable enforcement

While enforcement is off, identities without a `userHash` are still accepted (legacy behavior), but a **provided** hash is always validated — deploy step 3, watch the browser console for `[KLinkChat]` warnings, and once clean, turn enforcement on:

```bash
curl -X PUT "https://apigw.klinkcx.com/api/v1/webchat/channels/<channelId>/identity-verification" \
  -H "Authorization: Bearer kpt_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "enforce": true }'
```

You can combine both settings: `{ "enforce": true, "requireToken": true }`.

From then on, any `authUser` without a valid `userToken` or `userHash` is rejected: the visitor is treated as anonymous, the server logs the rejection, and the widget prints the reason to the browser console. Check status at any time:

```bash
curl "https://apigw.klinkcx.com/api/v1/webchat/channels/<channelId>/identity-verification" \
  -H "Authorization: Bearer kpt_xxxxxxxx"
# => { "requestId": "…", "secretConfigured": true, "enforceIdentityVerification": true, "requireIdentityToken": false }
```

### Behavior summary

| Channel state               | `authUser` without hash | `authUser` with valid hash      | `authUser` with invalid hash    |
| --------------------------- | ----------------------- | ------------------------------- | ------------------------------- |
| No secret configured        | trusted (legacy)        | trusted (legacy, cannot verify) | trusted (legacy, cannot verify) |
| Secret set, enforcement off | trusted (legacy)        | verified ✓                      | rejected → anonymous            |
| Secret set, enforcement on  | rejected → anonymous    | verified ✓                      | rejected → anonymous            |

Here "hash" means a `userHash` that correctly signs the identity value. A hash that signs a different value (e.g. the email when `identity` is set) is treated as invalid.

Additional guarantees, regardless of enforcement:

* A user record that was ever verified can only be matched or updated by another verified claim — an unverified claim can never take over a verified user's conversation history or overwrite their profile.
* Message sender identity is stamped server-side from the registered session; a client cannot send messages as someone else.
* **Shared devices are safe.** Each identity has its own conversation. Someone who logs in after another user on the same device never sees the previous user's messages. Call `klinkChatSDK.reset()` on logout (or use `resetVisitor=true` in the mobile app) so the next guest starts fresh too.
* Switching a channel's identity strategy (e.g. from email to a customer id) keys returning users differently, so the same person may start a fresh record. Pick one identity value per channel and keep it stable.

### Notes

* Rotating a secret invalidates hashes and tokens made with the old one, and ends active sessions. Deploy the new secret to your server at the same time; sessions registered in between fall back to anonymous rather than breaking the widget.
* The API endpoints use the same `kpt_` API token you already use for the K-Link public API, and only accept webchat channels that belong to your account.
