Skip to content

Single sign-on (SSO)

Let readers comment using the identity they already have on your site. Connecting an existing EchoThread account requires that account holder's approval. Available on the Pro plan and above.

Last updated · Get your signing secret from a site's Settings → SSO page in the dashboard.

Overview

Your server HMAC-signs a small identity payload with your site's SSO secret and hands it to the widget through the embed snippet. A valid signature can sign in a new publisher identity or an identity already connected to your site. Asserting an existing EchoThread account's email does not grant access to that account: its holder must sign in and approve the connection first.

Identity in this flow belongs to your site, not to EchoThread: there's no sign-out control on the widget itself, because signing a visitor out is something your own site's session already controls.

SSO requires the Pro plan or above. It isn't available on Hobby or Starter. See pricing for plan details.

Connecting an existing account

When approval is needed, the widget offers a connection window on EchoThread. The reader signs in using their existing EchoThread method, checks the publisher and the account shown, then chooses Approve connection. Signing in, refreshing, or opening an email link never approves it automatically. A different account cannot approve the request; the reader can switch accounts or cancel.

Keep the publisher window open while approving. If the connection window is blocked, closed, expired, or has already been used, start again from the publisher's sign-in option. The widget handles the one-time return to the original site; publishers should keep using fresh signed payloads and the current widget.

Later sign-ins resolve the approved site and external user ID. Publisher assertions do not overwrite the holder's existing profile or merge accounts and comments. The holder can keep using their normal EchoThread sign-in and profile settings.

Readers can review and disconnect publishers under Security → Publisher connections. Disconnecting revokes that publisher connection and its outstanding sign-in sessions. Other approved publishers and the holder's ordinary EchoThread account remain available.

The identity payload

The payload is a JSON object with exactly four fields:

FieldMeaning
idYour own user id for this person — opaque to EchoThread, unique within your site. Never EchoThread's internal id.
emailTheir email address. An existing EchoThread account requires its holder to approve the connection; this assertion does not prove mailbox ownership.
nameDisplay name for a new publisher identity. Existing EchoThread profiles are preserved.
avatarURL of their avatar image. Optional — send an empty string if you don't have one.
{
  "id": "12345",
  "email": "jamie@example.com",
  "name": "Jamie Reader",
  "avatar": "https://example.com/avatars/jamie.png"
}

Sign the exact bytes you send. The payload is hashed as the literal string you produce — never re-serialize it (re-encoding, re-ordering keys, or changing whitespace) between signing it and putting it in the embed snippet, or the signature won't match what EchoThread recomputes.

Signing the payload

Sign with your site's SSO secret (from Settings → SSO in the dashboard — never the public API key that ships in your embed snippet). The signature covers the payload and a Unix timestamp, joined with a literal period: {payload}.{timestamp}, HMAC-SHA256'd, hex-encoded.

Keep the SSO secret server-side. Unlike your site's API key, the SSO secret must never appear in a page's HTML or client-side JavaScript — anyone who has it can forge publisher identities and use connections already approved for your site.

Node.js

const crypto = require('crypto');

function buildEchoThreadSSO(secret, user) {
  // Build the exact string you'll sign — and the exact string you'll
  // send. Don't re-serialize it after this.
  const payload = JSON.stringify({
    id: String(user.id),
    email: user.email,
    name: user.name,
    avatar: user.avatarUrl || '',
  });
  const timestamp = Math.floor(Date.now() / 1000);

  const sig = crypto
    .createHmac('sha256', secret)
    .update(`${payload}.${timestamp}`)
    .digest('hex');

  return { payload, timestamp, sig };
}

// At render time, for the currently signed-in visitor:
const { payload, timestamp, sig } = buildEchoThreadSSO(
  process.env.ECHOTHREAD_SSO_SECRET,
  currentUser
);

PHP

<?php

function build_echothread_sso(string $secret, array $user): array {
    // Build the exact string you'll sign — and the exact string you'll
    // send. Don't re-serialize it after this.
    $payload = json_encode([
        'id'     => (string) $user['id'],
        'email'  => $user['email'],
        'name'   => $user['name'],
        'avatar' => $user['avatar_url'] ?? '',
    ]);
    $timestamp = time();

    $sig = hash_hmac('sha256', "{$payload}.{$timestamp}", $secret);

    return compact('payload', 'timestamp', 'sig');
}

// At render time, for the currently signed-in visitor:
['payload' => $payload, 'timestamp' => $timestamp, 'sig' => $sig] =
    build_echothread_sso(getenv('ECHOTHREAD_SSO_SECRET'), $current_user);
?>

Passing it to the widget

Render the payload, timestamp, and signature onto the embed container as three data attributes, alongside your usual data-api-key and data-shortname:

<div id="echothread"
  data-api-key="YOUR_API_KEY"
  data-shortname="YOUR_SITE_SHORTNAME"
  data-sso-payload="<?= htmlspecialchars($payload, ENT_QUOTES) ?>"
  data-sso-timestamp="<?= $timestamp ?>"
  data-sso-sig="<?= $sig ?>"
></div>
<script src="https://cdn.echothread.io/widget.js" async></script>
  • data-sso-payload — the exact JSON string you signed.
  • data-sso-timestamp — the Unix timestamp (seconds) you signed alongside it.
  • data-sso-sig — the hex-encoded HMAC-SHA256 signature.

All three must be present or the widget skips SSO entirely and falls back to its normal anonymous/login flow — there's no partial state. Render them fresh on every page load; don't cache a signed payload across requests (see the timestamp window below).

The 5-minute timestamp window

A signed payload is only valid for 5 minutes in either direction of the current time — both a stale payload and a payload forward-dated for later replay are rejected. Sign the timestamp at render time, right before the page is served; a payload built well ahead of time (for example during a build step) will have expired by the time a visitor loads the page.

Each (site, signature) pair is also only accepted once, so a captured payload can't be replayed a second time even inside that 5-minute window.

Rotating your secret

Rotate your SSO secret from Settings → SSO whenever you need to — for example after a suspected leak, or as routine hygiene. Rotating mints a new secret immediately and keeps the previous one valid for a 24-hour overlap window.

That overlap exists because your deploy that picks up the new secret isn't synchronized with ours: without it, rotating would break every SSO login signed with the old secret the instant you clicked rotate, until your own redeploy finished. With the 24-hour window, you can rotate, redeploy your backend with the new secret at your own pace, and readers keep signing in without interruption in between.

After 24 hours, the old secret stops working. Make sure your redeploy lands inside that window, or SSO logins will start failing (readers simply fall back to the normal anonymous/login flow — comments still render).