---
url: https://docs.comnto.com/integration/sso.md
description: >-
  Signing your users into the widget with an HMAC-SHA256 token built on your
  server: payload fields, Node/PHP/Python/Ruby/Go examples, delivery, refresh,
  and SSO-only mode.
---

# Single Sign-On (SSO)

Comnto supports seamless Single Sign-On (SSO), allowing your existing users to comment without creating new accounts. Your backend issues a short HMAC-signed token for each authenticated user, and the embedded widget sends that token to Comnto’s API to log the user in and receive normal access and refresh tokens — just like a regular login.

## Requirements

* Enable **SSO** (and optionally **Login button visibility**) in the dashboard.
* You need the site’s `sso_secret`. Keep it on the server—never ship it to the browser.
* Tokens must be generated on demand, over HTTPS, and should expire within a few minutes.

## Payload & signature

Build a JSON payload with the following fields:

| Field | Description |
| --- | --- |
| `site` | Your Comnto site id. **Must** match the site-id in the embed. |
| `user_id` | Your internal user identifier (string or number). Reuse the same value for the same person. |
| `email` | Optional but recommended. Lowercase string. |
| `name` | Display name shown next to comments. |
| `username` | Optional preferred username. Comnto picks a unique alternative if the value is taken. |
| `avatar` | Optional external avatar URL. Comnto queues an import after the first login. |
| `iat` | Issued-at timestamp (Unix seconds). |
| `exp` | Expiration timestamp (Unix seconds). Keep the window short (e.g. `exp = iat + 600`). |

Sign the payload like this:

```
token = base64url( JSON payload ) + '.' + hex( HMAC_SHA256(base64url payload, sso_secret) )
```

Comnto validates the signature, checks `iat`/`exp` (±5 minute leeway), ensures `site` matches, then either creates or updates the user.

## Token generation examples

::: code-group

```js [Node.js]
import crypto from 'node:crypto';

const base64url = (input) =>
  Buffer.from(input)
    .toString('base64')
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');

export function makeComntoSsoToken({ siteId, userId, email, name, username, avatar, secret }) {
  const now = Math.floor(Date.now() / 1000);
  const payload = {
    site: siteId,
    user_id: String(userId),
    email,
    name,
    username,
    avatar,
    iat: now,
    exp: now + 600,
  };

  const body = base64url(JSON.stringify(payload));
  const signature = crypto.createHmac('sha256', secret).update(body).digest('hex');
  return `${body}.${signature}`;
}
```

```php [PHP]
<?php
function base64url($data) {
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function make_sso_token($siteId, $userId, $email, $name, $username, $avatar, $secret) {
  $now = time();
  $payload = json_encode([
    'site' => (string)$siteId,
    'user_id' => (string)$userId,
    'email' => $email,
    'name' => $name,
    'username' => $username,
    'avatar' => $avatar,
    'iat' => $now,
    'exp' => $now + 600
  ]);
  $body = base64url($payload);
  $sig = hash_hmac('sha256', $body, $secret);
  return $body . '.' . $sig;
}
```

```python [Python]
import base64, hmac, hashlib, json, time

def base64url(b: bytes) -> str:
    return base64.urlsafe_b64encode(b).decode().rstrip('=')

def make_sso_token(site_id: str, user_id: str, email: str, name: str, username: str, avatar: str, secret: str) -> str:
    now = int(time.time())
    payload = {
        'site': site_id,
        'user_id': str(user_id),
        'email': email,
        'name': name,
        'username': username,
        'avatar': avatar,
        'iat': now,
        'exp': now + 600,
    }
    body = base64url(json.dumps(payload).encode())
    sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
    return f"{body}.{sig}"
```

```rb [Ruby]
require 'json'
require 'openssl'
require 'base64'

def base64url(str)
  Base64.urlsafe_encode64(str).gsub('=', '')
end

def make_sso_token(site_id:, user_id:, email:, name:, username:, avatar:, secret:)
  now = Time.now.to_i
  payload = {
    site: site_id,
    user_id: user_id.to_s,
    email: email,
    name: name,
    username: username,
    avatar: avatar,
    iat: now,
    exp: now + 600
  }
  body = base64url(payload.to_json)
  sig = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
  "#{body}.#{sig}"
end
```

```go [Go]
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "time"
)

func base64url(b []byte) string {
    return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b)
}

func makeSSOToken(siteId, userID, email, name, username, avatar, secret string) (string, error) {
    now := time.Now().Unix()
    payload := map[string]any{
        "site":    siteId,
        "user_id": userID,
        "email":        email,
        "name":         name,
        "username":     username,
        "avatar":       avatar,
        "iat":          now,
        "exp":          now + 600,
    }
    bodyBytes, _ := json.Marshal(payload)
    body := base64url(bodyBytes)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(body))
    sig := hex.EncodeToString(mac.Sum(nil))
    return fmt.Sprintf("%s.%s", body, sig), nil
}
```

:::

## Delivering the token to the widget

The token must only be produced on the server and delivered to the page over HTTPS.

### Option 1 — Inline attribute

```html
<div
  id="comments"
  data-comnto
  data-site="my-blog"
  data-sso="{{SSO_TOKEN_FROM_SERVER}}"
  data-login-button-visible
></div>
<script src="https://comnto.com/embed.js" defer></script>
```

### Option 2 — Programmatic call

```html
<div id="comments"></div>
<script src="https://comnto.com/embed.js" defer></script>
<script>
  window.addEventListener('DOMContentLoaded', async () => {
    const token = await fetch('/comnto/sso-token', { credentials: 'include' }).then((res) => res.text());
    window.comnto({
      el: '#comments',
      site: 'my-blog',
      sso: token,
      login_button_visible: true,
    });
  });
</script>
```

## Refreshing / logging out

* Issue short tokens (5–10 minutes). When you produce a new one, call `widget.reload({ sso: freshToken })`.
* Send `widget.reload({ sso: '' })` to revoke the session.
* The widget automatically POSTs the token to `POST /v1/{site_id}/sso`, receives a JWT + refresh token, and stores them internally. You do **not** need to call that endpoint yourself.

## SSO-only mode & login button

Enabling SSO-only mode from the dashboard prevents normal login through Comnto’s built-in form. Keep `login_button_visible` on if you still want the UI to show a “Login” action. Listen for the `comnto-login` event to launch your own authentication window:

```js
window.addEventListener('comnto-login', () => {
  openMyLoginModal(); // trigger your auth flow, then refresh the SSO token
});
```

Once your flow succeeds, fetch a new token and reload the widget or respond to the `comnto:auth` message that the widget broadcasts.

## Security checklist

* Always generate tokens on the server; never expose the `sso_secret`.
* Use short-lived tokens (iat / exp) to keep sessions limited in time.

With these steps in place the embed signs users in using your identity provider while Comnto handles comments, moderation, and notifications.
