Skip to content

Single Sign-On (SSO) ​

The widget can use a session from your parent website or product to sign an already-linked user into Zeeker directly. Your server mints a short-lived HS256 JWT; the browser hands it to the widget, and Zeeker exchanges it for a Zeeker session.

This is the JWT bridge flow. OIDC is reserved for a future release.

SSO provider configuration belongs to the community. Configure the secret, allowed domains, label, icon, redirect URL, and enablement once in the Admin Center. Each embed then chooses whether it permits Community SSO, Zeeker login, or SSO or Zeeker login, and when that permitted method should begin.

This is an inherited capability: an embed can narrow an enabled community identity path but cannot configure a second provider, use disabled SSO, or bypass community visibility/membership. The rule is identical for every Element preset because presets do not own authentication.

User experience ​

Identity stateResult
The parent identity is already linked to a Zeeker accountThe widget signs the user in immediately. No Zeeker login screen is shown.
No Zeeker account exists for the parent emailZeeker ID opens as a blocking in-page screen. Verified email and a required username are shown together with optional display name, city, and interests. The screen cannot be dismissed until the required account setup succeeds.
A Zeeker account already uses the parent email but is not linkedZeeker ID asks the user to verify the existing account before linking it. This prevents an SSO assertion from silently taking over an existing Zeeker identity.

First-time accounts are never silently auto-created. After the one-time Zeeker ID step, later assertions for the same community and stable parent sub take the direct-login path.

Configure the community ​

In the admin center, select the community and open SSO.

  1. Choose JWT Bridge.
  2. Set the sign-in button label and optional icon.
  3. Generate or paste an HMAC secret and keep it in your server-side secret store.
  4. Optionally restrict accepted email domains.
  5. Set an HTTPS Redirect URL with a real handler path that can mint the same JWT for community-home, post, and deep-link sign-in. It must redirect to the URL Zeeker supplies in its return query parameter.
  6. Save the draft, then select Test & enable SSO. Zeeker opens the real handoff, verifies its signed callback, and activates only that tested version.

The HMAC secret is never returned by the settings API after it is stored. Generating a new secret shows it once so you can copy it.

In the Embed configurator, set Sign-in method and When to sign in for each embed. The SSO choices stay disabled until this community-level connection is enabled.

Per-embed login modes ​

Configurator choiceRuntime valueBehavior
Zeeker loginzeekerUses the standard Zeeker identity flow.
Community SSOcommunity_ssoUses only the enabled parent-community SSO bridge.
SSO or Zeeker logineitherPermits the host bridge or standard Zeeker identity.
On gated actionon_actionDefers identity until a protected read or interaction needs it.
Automatically on loadon_loadEmits sso-required during boot so the host can identify its current user. Requires an SSO-capable sign-in method.
Managed by host apphost_managedNever opens a Zeeker identity surface for a gated action. Emits auth-required; the host owns the prompt and calls identify(jwt). Requires an SSO-capable sign-in method.

Zeeker-only login is always action-gated. Automatic and host-managed modes require an enabled community SSO connection and a sign-in method of community_sso or either.

Sign-in prompt behavior ​

Enabled community SSO is the primary option on the first Zeeker ID screen. This remains true when sign-in opens from the community home, a post or deep link, a page discussion, a community embed, or an auth-gated action such as posting, replying, reacting, saving or bookmarking, and joining.

The configured SSO label and icon are shown first. Phone, email, and social sign-in methods are collapsed under Other Options. Disabling community SSO restores the standard sign-in screen.

Widget prompts obtain identity through the host page's sso-required handler below. Standalone community homes, custom domains, and post deep links instead require the community's HTTPS Redirect URL to perform the parent-site JWT handoff. The public SSO switch alone cannot identify a visitor.

Mint the parent JWT ​

Sign the JWT on your server. Never expose the HMAC secret to browser code.

ClaimRequiredMeaning
subYesStable user identifier from your product. Do not use a value that changes with the user's email or display name.
emailYesParent account email. It is normalized to lowercase and checked against the optional domain allow-list.
nameNoDisplay-name prefill for first-time Zeeker ID setup.
avatar_urlNoAvatar prefill for the Zeeker account.
iatYesIssued-at time in Unix seconds. The activation test requires a freshly minted token.
expYesExpiration time in Unix seconds, no more than five minutes after iat.
nbfNoNot-before time in Unix seconds.

The JWT header must use "alg": "HS256". Issuer, audience, key-id, and username claims are not required by the current JWT bridge.

js
import jwt from 'jsonwebtoken';

const token = jwt.sign(
  {
    sub: user.id,
    email: user.email,
    name: user.displayName,
    avatar_url: user.avatarUrl,
  },
  process.env.ZEEKER_SSO_SECRET,
  { algorithm: 'HS256', expiresIn: '5m' },
);

Implement the direct handoff endpoint ​

The Redirect URL is a backend route, not your homepage and not a static HTML file. It receives a one-time return URL from Zeeker, authenticates the current user from your own server session, and redirects back with a freshly minted JWT. Keep the JWT in the URL fragment so it is not sent in the callback HTTP request.

js
import express from 'express';
import jwt from 'jsonwebtoken';

const app = express();
const allowedCallbacks = new Set([
  'https://id.zeeker.com/sso/callback',
  'https://admin.zeeker.com/sso/test-callback',
]);

app.get('/api/zeeker-sso', requireSignedInUser, (req, res) => {
  let callback;
  try {
    callback = new URL(String(req.query.return || ''));
  } catch {
    return res.status(400).send('Invalid return URL');
  }
  if (!allowedCallbacks.has(`${callback.origin}${callback.pathname}`)) {
    return res.status(400).send('Untrusted return URL');
  }

  const token = jwt.sign(
    {
      sub: req.user.id,
      email: req.user.email,
      name: req.user.displayName,
    },
    process.env.ZEEKER_SSO_SECRET,
    { algorithm: 'HS256', expiresIn: '5m' },
  );
  callback.hash = new URLSearchParams({ jwt: token }).toString();
  return res.redirect(303, callback.toString());
});

If your login flow redirects through another page, preserve the complete return value through that flow. Never hard-code a JWT into HTML or ship the HMAC secret to the browser. The Admin Center browser test must pass before the SSO button is published.

Connect the parent session to the widget ​

Fetch the signed JWT from an authenticated endpoint in your product, then call identify. The loader is normally asynchronous, so register through its ready event when window.zeeker is not installed yet:

js
function registerZeekerSso(zeeker) {
  zeeker.on('sso-required', async () => {
    try {
      const response = await fetch('/api/zeeker-sso-token', {
        credentials: 'include',
      });
      if (!response.ok) {
        throw new Error(`SSO token endpoint returned ${response.status}`);
      }

      const token = (await response.text()).trim();
      if (!token) throw new Error('SSO token endpoint returned an empty token');
      zeeker.identify(token);
    } catch (error) {
      console.error('[Zeeker SSO]', error);
    }
  });
}

if (window.zeeker) {
  registerZeekerSso(window.zeeker);
} else {
  window.addEventListener('zeeker:loader-ready', (event) => {
    registerZeekerSso(event.detail.api);
  }, { once: true });
}

An embed configured for Automatically on load emits sso-required on boot. You can also call window.zeeker.identify(token) yourself after your parent app confirms its session.

The same event is emitted when a user selects the SSO button in Zeeker ID. In nested layouts, such as Zeeker ID inside a community or post iframe inside the widget, each Zeeker-owned frame relays the request to the top-level widget. The host page should listen only through window.zeeker.on('sso-required', ...); it does not need to inspect or forward iframe messages itself.

The widget also emits sso-required with automatic: true and reason: 'session_refresh' two minutes before its access token expires. The handler above renews the session silently by fetching a fresh JWT. Do not reuse the previous short-lived assertion or open a login modal for automatic renewal. If the publisher session has ended, call window.zeeker.logout(). Temporary request failures retry at most every 30 seconds while the page is visible. A successful renewal keeps the same signed-in member and the current discussion or chat open.

Ordinary Zeeker sign-ins renew silently through Zeeker ID. Community chat opened in a separate tab uses the community's verified Redirect URL for SSO; the community widget keeps its chat destinations in the widget so the publisher's existing SSO bridge remains available.

The widget sends { configId, jwt } to POST /api/embed/sso/exchange. The response is one of:

StatusMeaning
okThe parent identity is linked. The response contains accessToken and user, and login completes directly.
needs_onboardingA new account needs the required Zeeker ID completion screen. The response contains a short-lived partialToken and prefill data.
needs_attachThe email matches an existing unlinked Zeeker account. The response contains a short-lived partialToken and existing-account preview.

Partial tokens are one-time, expire quickly, and travel to Zeeker ID in the URL fragment rather than the request path.

Host-managed sign-in ​

Use Managed by host app when your website or native shell must own the login UI. Register for auth-required, obtain a fresh JWT from your authenticated backend, and pass it to the same identify method:

js
function registerHostManagedAuth(zeeker) {
  zeeker.on('auth-required', async ({ reason, requestId, communityId }) => {
    const response = await fetch('/api/zeeker-sso-token', {
      credentials: 'include',
    });
    if (!response.ok) throw new Error(`SSO token request failed: ${response.status}`);
    zeeker.identify((await response.text()).trim());
  });
}

if (window.zeeker) {
  registerHostManagedAuth(window.zeeker);
} else {
  window.addEventListener('zeeker:loader-ready', event => {
    registerHostManagedAuth(event.detail.api);
  }, { once: true });
}

requestId and communityId are present when the gated request originated in the active nested Zeeker surface. The loader validates the source window, origin, community, and correlation before returning the authenticated session to that surface. The publisher should use the public event and must not forward raw iframe messages itself.

Events ​

js
window.zeeker.on('auth-required', detail => {
  // Host-managed mode: authenticate in the host, then call identify(jwt).
});
window.zeeker.on('sso-success', ({ user }) => console.log('signed in as', user));
window.zeeker.on('sso-error', ({ error }) => console.error(error));
window.zeeker.on('sso-pending', ({ kind }) => {
  // kind is "onboarding" or "attach" while Zeeker ID owns the handoff.
});
window.zeeker.on('sso-cancelled', ({ kind }) => {
  // Only existing-account attachment can be cancelled.
});

See the complete embed event reference.

Common errors ​

ErrorCauseFix
Invalid or expired SSO tokenWrong HMAC secret, malformed token, unsupported algorithm, or expired/not-yet-valid token.Mint a fresh HS256 token with the configured secret.
SSO token missing required claims (sub, email)The stable parent id or email is missing.Include non-empty sub and email string claims.
Email domain not allowed for this SSO connectionThe email does not match the configured allow-list.Update the allow-list or use the correct parent identity.
Username is already takenThe user's requested Zeeker username became unavailable during setup.Choose another username; the one-time handoff remains usable.
SSO not enabled for this widgetCommunity SSO is disabled.Enable SSO in the community's Admin Center settings.
SSO remains in DraftThe handoff is missing, is a homepage/static page, returned no JWT, or signed with a different secret.Implement the backend handler above, then use Test & enable SSO.
Selecting SSO in an embed does nothingThe host registered before the async loader existed, or its token endpoint is absent/failing.Use the loader-ready pattern above and verify the endpoint returns a non-empty JWT with HTTP 200.

Security notes ​

  • Keep the HMAC secret on your backend only.
  • Use short-lived JWTs and a stable, opaque parent sub.
  • Treat the email as verified parent identity data; do not mint a token until your own session is authenticated.
  • Zeeker never links an SSO assertion to an existing same-email Zeeker account without re-verification.
  • Validate sso-success and sso-error in your integration telemetry so failed parent-session exchanges are visible.