Skip to content

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:

FieldDescription
siteYour Comnto site id. Must match the site-id in the embed.
user_idYour internal user identifier (string or number). Reuse the same value for the same person.
emailOptional but recommended. Lowercase string.
nameDisplay name shown next to comments.
usernameOptional preferred username. Comnto picks a unique alternative if the value is taken.
avatarOptional external avatar URL. Comnto queues an import after the first login.
iatIssued-at timestamp (Unix seconds).
expExpiration 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

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
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
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
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
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.