> For the complete documentation index, see [llms.txt](https://gdplabs.gitbook.io/glchat/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gdplabs.gitbook.io/glchat/developer-documentation/partner-sso.md).

# Partner SSO

{% hint style="info" %}
**When to use:** On your **backend** when you need to mint a one-time SSO token for **IdP-initiated** login. Partners (IdPs, portals, or backend services) call GLChat with an HMAC-signed request; the user's client then completes authentication. Do not expose `consumer_secret` to browsers or mobile apps.
{% endhint %}

This guide walks through integrating **IdP-initiated SSO** into GLChat. Three actors are involved:

* Your **Partner Backend**
* The **GLChat Backend**
* The user's **Browser/App**

## Integration Flow

{% code title="Sequence Diagram" %}

```mermaid
sequenceDiagram
    participant P as Partner Website
    participant G as GLChat Backend
    participant B as Widget

    Note over P: User clicks "Login to Partner Website"
    P->>G: POST /api/v1/sso/token (HMAC Signed)
    G-->>P: Return One-Time Token
    P-->>B: Handoff Token + Tenant Slug
    Note over B: Redirect or Widget Load
    B->>G: POST /api/v1/sso/authenticate
    G-->>B: Return User Session (JWT)
```

{% endcode %}

1. **Mint token** — Your server requests a short-lived, one-time token from GLChat.
2. **Handoff** — You pass that token and the **tenant** slug to the user's client.
3. **Authenticate** — The client exchanges the token for a session (JWT).

<details>

<summary>Prerequisites</summary>

* **Credentials:** `consumer_key` (public) and `consumer_secret` (private) from your GLChat administrator.
* **Tenant:** The organization **slug** you are integrating with.
* **HTTPS:** Production traffic must use TLS 1.2+.

</details>

{% hint style="danger" %}
**Security:** Never share `consumer_secret` or embed it in client-side code (JavaScript, mobile). It must live only on your backend.
{% endhint %}

## Signature Format

Partners prove authenticity by signing the **exact string** GLChat will verify. Compute:

```
HMAC-SHA256(consumer_secret, signing_string)
```

Where the **signing string** is:

```
timestamp|consumer_key|payload
```

| Component         | Description                                                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `consumer_secret` | Your private secret (from registration). Used only as the HMAC key, never sent in the payload.                                                    |
| `timestamp`       | ISO 8601 UTC (e.g. `2026-05-04T12:00:00+00:00` or `...Z`).                                                                                        |
| `consumer_key`    | Your public consumer key.                                                                                                                         |
| `payload`         | **Stringified JSON** of user fields (`email`, `display_name`, `external_id`) with stable serialization (see [Common pitfalls](#common-pitfalls)). |
| \`                | \` (pipe)                                                                                                                                         |

The **signature** you send is the HMAC-SHA256 digest, **hex-encoded** (lowercase hex is typical; match what your runtime produces).

## Minimal Signing (Partner Side)

```python
timestamp = datetime.now(timezone.utc).isoformat()
signing_string = f"{timestamp}|{consumer_key}|{payload_str}"
signature = hmac.new(
    consumer_secret.encode("utf-8"),
    signing_string.encode("utf-8"),
    hashlib.sha256,
).hexdigest()
```

Then send `consumer_key`, `signature`, `timestamp`, and `payload` (the same string you signed) in the JSON body of `POST /api/v1/sso/token`.

## Step-by-Step

{% stepper %}
{% step %}

#### Build the payload string

Create a JSON object with at least **`email`** or **`external_id`**, plus optional fields such as `display_name`. Serialize to a **single canonical string** (stable key order and no stray whitespace for predictable signatures—see pitfalls).

```python
user_payload = {
    "email": "jane.doe@example.com",
    "display_name": "Jane Doe",
    "external_id": "user_9988",
}
payload_str = json.dumps(user_payload, separators=(",", ":"))
```

{% endstep %}

{% step %}

#### Compute the signature

Use the [Signature format](#signature-format): `timestamp|consumer_key|payload` as UTF-8 bytes, keyed by `consumer_secret`, SHA-256, hex digest.
{% endstep %}

{% step %}

#### Call the token endpoint

`POST https://{GLCHAT_HOST}/api/v1/sso/token` with JSON:

```json
{
  "consumer_key": "<your consumer_key>",
  "signature": "<hex hmac>",
  "timestamp": "<same iso timestamp you signed>",
  "payload": "<exact same string as in the signing_string>"
}
```

On success you receive `token` and `expires_in` (seconds).
{% endstep %}

{% step %}

#### Hand off to the client

Pass the `token` and **tenant** slug to the browser through a secure channel via the [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) method. The message should be sent to:

```
https://{GLCHAT_WEB_HOST}/idp
```

The message must contain a JSON-stringified object with the following schema:

| Property       | Type     | Description                                                         |
| -------------- | -------- | ------------------------------------------------------------------- |
| `type`         | `string` | Type of the message. For IdP SSO purpose, set this to `'auth-idp'`. |
| `token`        | `string` | The SSO token received from the `/api/v1/sso/token` endpoint.       |
| `organization` | `string` | The slug of the tenant.                                             |

The client should call GLChat's authenticate step immediately *after* the page has been loaded successfully; tokens are short-lived and **single-use**.

The authentication page will fire a `message` event after it has been loaded successfully with the following JSON-stringified payload:

| Property | Type     | Description                                                                                         |
| -------- | -------- | --------------------------------------------------------------------------------------------------- |
| `type`   | `string` | <p>Type of the message.</p><p>In this case, the value will always be <code>auth-idp-done</code></p> |

{% hint style="warning" %}
For security purposes, this endpoint only listens to events that come from trusted hostnames defined by `NEXT_PUBLIC_FEATURE_TRUSTED_EVENT_DOMAINS` feature config.
{% endhint %}
{% endstep %}

{% step %}

### Logging Out GLChat

When you're about to conduct a logout flow in your application, you must pass a message event through the [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) method to the `iframe` to conduct a logout process in GLChat. Otherwise, the currently logged in account will persist in the next session.

The message must contain a JSON-stringified object with the following schema:

| Property      | Type     | Description                                                                                  |
| ------------- | -------- | -------------------------------------------------------------------------------------------- |
| `type`        | `string` | <p>Type of the message.</p><p>For logout purpose, set this to <code>'logout-idp'</code>.</p> |
| {% endstep %} |          |                                                                                              |

{% step %}
After the logout event has succeeded, GLChat will fire an additional message to indicate that GLChat has been logged out successfully.

The message is sent in a JSON-stringified format via [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) , which can be listened through `window.addEventListener('message')` method with the following schema:

| Property         | Type     | Description                                                                          |
| ---------------- | -------- | ------------------------------------------------------------------------------------ |
| `type`           | `string` | <p>Type of the message.</p><p>Will always be valued <code>logout-idp-done</code></p> |
| {% endstep %}    |          |                                                                                      |
| {% endstepper %} |          |                                                                                      |

## Full Examples

{% tabs %}
{% tab title="Python" %}

```python
import hashlib
import hmac
import json
import requests
from datetime import datetime, timezone

HOST = "https://api.glchat.io"
CONSUMER_KEY = "your_key"
CONSUMER_SECRET = "your_secret"

user_payload = {
    "email": "jane.doe@example.com",
    "display_name": "Jane Doe",
    "external_id": "user_9988",
}
payload_str = json.dumps(user_payload, separators=(",", ":"))

timestamp = datetime.now(timezone.utc).isoformat()
signing_string = f"{timestamp}|{CONSUMER_KEY}|{payload_str}"
signature = hmac.new(
    CONSUMER_SECRET.encode("utf-8"),
    signing_string.encode("utf-8"),
    hashlib.sha256,
).hexdigest()

response = requests.post(
    f"{HOST}/api/v1/sso/token",
    json={
        "consumer_key": CONSUMER_KEY,
        "signature": signature,
        "timestamp": timestamp,
        "payload": payload_str,
    },
    timeout=30,
)
response.raise_for_status()
token_data = response.json()
print(f"One-Time Token: {token_data['token']}")
```

{% endtab %}

{% tab title="Node.js" %}

```javascript
const crypto = require("crypto");
const axios = require("axios");

async function getSsoToken() {
  const host = "https://api.glchat.io";
  const consumerKey = "your_key";
  const consumerSecret = "your_secret";

  const payload = JSON.stringify({
    email: "jane.doe@example.com",
    display_name: "Jane Doe",
  });

  const timestamp = new Date().toISOString();
  const signingString = `${timestamp}|${consumerKey}|${payload}`;

  const signature = crypto
    .createHmac("sha256", consumerSecret)
    .update(signingString)
    .digest("hex");

  const { data } = await axios.post(`${host}/api/v1/sso/token`, {
    consumer_key: consumerKey,
    signature,
    timestamp,
    payload,
  });

  return data.token;
}
```

{% endtab %}

{% tab title="cURL" %}

```bash
curl -X POST https://api.glchat.io/api/v1/sso/token \
  -H "Content-Type: application/json" \
  -d '{
    "consumer_key": "your_key",
    "signature": "computed_hmac_hex",
    "timestamp": "2026-05-04T12:00:00Z",
    "payload": "{\"email\":\"jane.doe@example.com\"}"
  }'
```

{% endtab %}
{% endtabs %}

### Client-Side Operations

#### Sending SSO Request

The following snippet shows an example procedure of authenticating with IdP-initiated SSO to an existing GLChat widget.

{% code title="client.ts" overflow="wrap" lineNumbers="true" %}

```typescript
const widget = document.querySelector('.glchat-widget');
// Set this to the hostname of your iframe
const targetHost = 'https://www.example.com';

const payload = {
  type: 'auth-idp',
  token: '<sso_token>',
  organization: '<tenant_slug>',
};

if (widget) {
  const internalIframe = widget.shadowRoot?.querySelector('iframe');

  internalIframe?.contentWindow?.postMessage(
    JSON.stringify(payload),
    targetHost
  );
}
```

{% endcode %}

#### Logging Out

The following snippet shows an example procedure of conducting logout process in the GLChat widget triggered from your application.

<pre class="language-typescript" data-title="client.ts" data-overflow="wrap" data-line-numbers><code class="lang-typescript">const widget = document.querySelector('.glchat-widget');
// Set this to the hostname of your iframe
const targetHost = 'https://www.example.com';

const payload = {
  type: 'logout-idp',
};

if (widget) {
  const internalIframe = widget.shadowRoot?.querySelector('iframe');

  internalIframe?.contentWindow?.postMessage(
    JSON.stringify(payload),
    targetHost,
<strong>  );
</strong>}

// Logout process can be confirmed by:

<strong>window.addEventListener('message', (e) => {
</strong>  const payload = JSON.parse(e.data);
  
  if (payload.type === 'logout-idp-done') {
    // Logout in GLChat succeeded!
  }
});
</code></pre>

## API Reference

### `POST /api/v1/sso/token`

| Field          | Type   | Description                                                                  |
| -------------- | ------ | ---------------------------------------------------------------------------- |
| `consumer_key` | string | Your public identifier.                                                      |
| `signature`    | string | Hex-encoded HMAC-SHA256 over the signing string \`timestamp                  |
| `timestamp`    | string | ISO-8601 timestamp (UTC).                                                    |
| `payload`      | string | Stringified JSON with user details (`email`, `display_name`, `external_id`). |

**Success (200 OK):**

```json
{
  "token": "sso_tk_1234567890abcdef",
  "expires_in": 60
}
```

## Common Pitfalls

| Symptom / Pitfall                                    | What to Do                                                                                                                                                          |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Signature always invalid                             | Use signing string `timestamp\|consumer_key\|payload` with the **same** `payload` string you send in the JSON body—no extra spaces or different JSON serialization. |
| `401 Unauthorized`                                   | Re-check HMAC input order and that the `payload` field equals the signed string.                                                                                    |
| Timestamp rejected / `403 Forbidden`                 | Use ISO 8601 UTC; sync time with NTP. GLChat rejects skew typically beyond **5 minutes**.                                                                           |
| Works in dev, fails in prod                          | Confirm environment variables for `consumer_secret` and `consumer_key` match the registered partner.                                                                |
| Token already used / expired / `401` on authenticate | Tokens are **single-use** and short TTL (\~60s). Mint again if the user refreshes before finishing login.                                                           |
| `422 Unprocessable`                                  | Include at least `email` or `external_id` in the JSON inside `payload`.                                                                                             |

## Security and Production Checklist

* [ ] **HTTPS only** for all API calls (TLS 1.2+).
* [ ] **Secrets:** Store `consumer_secret` in environment variables or a secret manager—never in git or client bundles.
* [ ] **Clock sync:** NTP on servers that sign requests.
* [ ] **Stable JSON:** Canonical stringification for `payload` (e.g. `separators=(",", ":")` in Python) so signatures match every time.
* [ ] **Single use:** Plan to **remint** if authentication does not complete before expiry.

{% hint style="success" %}
*Last updated: May 2026*
{% endhint %}
