Skip to documentation

Ideas Portal JWT SSO

Set up Ideas Portal JWT SSO

Use this guide to let users who are already signed in to your product open your Zentrik Ideas Portal without creating a separate password. Your app authenticates the current user, signs a short-lived JWT, and sends the browser to Zentrik.

The Ideas Portal is a customer-facing place for users to vote on ideas and submit requests. Authentication is owned by your product: if the user can access your app, your app can issue a short-lived token that proves who they are to Zentrik.

The connection has three configured values in Zentrik:

  • Callback URL: generated by Zentrik; your app redirects here with the signed JWT.
  • Remote login URL: hosted by your app; Zentrik sends unauthenticated users here.
  • Remote logout URL: hosted by your app; Zentrik sends users here after they sign out of the portal.

Zentrik validates the token, links the user to an account, sets a portal session cookie, and redirects the user into the portal.

Two launch modes are supported:

  • Portal-initiated login: the user opens the portal, which redirects to your remote login URL with a state value that must be returned to the callback.
  • Product-initiated login: a button in your authenticated product opens your login endpoint first, which can mint a fresh JWT and redirect directly to the alias-specific portal callback without state.

Before you start

Ask your Zentrik workspace admin for:

  • the portal URL, for example https://acme.ideas.zentrik.ai
  • the callback URL, for example https://acme.ideas.zentrik.ai/api/portal/auth/jwt/callback?jwt=<token>
  • the shared secret, shown once when the admin configures or rotates Product SSO

Your team provides:

  • a remote login URL in your app, for example https://app.example.com/zentrik/ideas/login
  • a remote logout URL in your app, for example https://app.example.com/logout
  • user identity fields: email, first name, last name, and optionally a stable user id
  • account identity fields if one user can belong to a customer account, tenant, practice, clinic, or organization

Portal-initiated login flow

In this mode, login starts at the Ideas Portal. Wait for Zentrik to redirect to your remote login URL before generating the JWT, because this request includes a state value that must round-trip back to the callback.

1. Portal redirects to your app

When an unauthenticated user opens the portal, Zentrik redirects the browser to your remote login URL.

http1 lines
GET https://app.example.com/zentrik/ideas/login?state=RANDOM_STATE&return_to=%2F

Your endpoint must:

  • require the user to be signed in to your app
  • preserve the exact state query parameter
  • preserve the return_to value if present
  • create the JWT only after you know the signed-in user

If the user is not signed in, send them through your normal product login first. After your product login succeeds, resume this same SSO endpoint with the original state and return_to.

2. Your app creates a JWT

Sign the JWT with HS256 using the shared secret from Zentrik.

Required claims:

  • iat: issued-at time as a Unix timestamp in seconds
  • jti: unique token id; generate a fresh value for every login attempt
  • email: signed-in user's email address
  • first_name: signed-in user's first name
  • last_name: signed-in user's last name

Recommended claims:

  • exp: expiration time, usually five minutes or less after iat
  • sub: stable user id from your product
  • accounts: one or more account objects if the user belongs to customer accounts or tenants

Zentrik rejects tokens that are not HS256, have stale iat, reuse a jti, or are missing the required identity claims.

3. Redirect back to Zentrik

Redirect the browser to the Zentrik callback URL with:

  • jwt: the signed token
  • state: the exact state value Zentrik sent to your remote login URL
  • return_to: the same portal path, if present
http1 lines
GET https://acme.ideas.zentrik.ai/api/portal/auth/jwt/callback?jwt=TOKEN&state=RANDOM_STATE&return_to=%2F

The return_to value must be a portal-relative path such as / or /request. Do not send a full external URL.

4. Zentrik creates the session

After validation, Zentrik:

  • finds or creates the portal identity for the email/user id
  • links the identity to one or more customer accounts when account claims are present
  • stores the portal session in a secure cookie
  • redirects the browser to return_to or the portal home

From that point on, the Ideas Portal session is handled by Zentrik.

Product-initiated direct launch

When the portal launcher lives inside your authenticated product, your product can initiate SSO directly. This is a supported callback contract and does not require a portal-generated state value.

First, have the product button open your own login endpoint with the desired portal destination:

http1 lines
GET https://app.example.com/zentrik/ideas/login?return_to=%2Frequest

Your endpoint must authenticate the current product user and mint a fresh JWT for that user. Redirect the browser to the alias-specific callback URL with jwt and return_to only:

http1 lines
GET https://acme.ideas.zentrik.ai/api/portal/auth/jwt/callback?jwt=TOKEN&return_to=%2Frequest

Callback parameters for this mode:

  • jwt: required; a freshly signed HS256 token
  • return_to: optional portal-relative destination; use /request to open the Request tab
  • state: omit it entirely; do not send an empty state parameter

Use the callback hostname assigned to your portal, such as acme.ideas.zentrik.ai. Without portal state, Zentrik resolves the correct SSO configuration from this alias-specific hostname.

After successful validation, Zentrik sets portal_session for the JWT identity. The new cookie uses the same host, name, and path, so it replaces any existing portal session from a previous user. An invalid, expired, or replayed JWT returns an authentication error and does not replace the existing session. An abandoned portal state cookie does not block this direct flow and is cleared after successful authentication.

Shared machines and product launchers

A valid Ideas Portal session normally skips SSO. This lets the same user reopen the portal without another redirect, but it also means Zentrik cannot detect that a different user has signed in to your product in the same browser.

For a button inside your authenticated product, use the product-initiated direct launch above. Because your endpoint mints a fresh JWT for the current product user on every launch, a successful callback replaces any previous portal session.

If the button must open the portal URL directly instead, append refresh_identity=1 to the launch URL:

http1 lines
GET https://acme.ideas.zentrik.ai/request?refresh_identity=1

For that launch, Zentrik:

  • bypasses the existing portal_session
  • redirects to your configured remote login URL with a new state
  • removes refresh_identity from return_to to prevent a redirect loop
  • replaces the portal session after your app returns a valid JWT for the current user

Your remote login endpoint must determine the currently signed-in product user when it receives the request and mint a fresh JWT for that user. Do not reuse a JWT from an earlier launch.

This SSO round trip occurs whenever the launcher includes refresh_identity=1. It should not show another login prompt when the same user still has a valid product session, because your remote login endpoint can immediately issue the JWT and redirect back to Zentrik.

Other query parameters and URL fragments are preserved. For example, /request?source=product&refresh_identity=1#new-request returns to /request?source=product#new-request after authentication.

Implementation

The remote login URL is a browser redirect endpoint, not a server-to-server API call. It should end by redirecting the browser back to the Zentrik callback URL.

JWT claims

Minimal payload:

json9 lines
{
  "iat": 1778770000,
  "exp": 1778770300,
  "jti": "6a3f0cf7-f01c-4b3c-9db3-94e7f263f726",
  "sub": "user_12345",
  "email": "jane@example.com",
  "first_name": "Jane",
  "last_name": "Rivera"
}

Payload with account context:

json16 lines
{
  "iat": 1778770000,
  "exp": 1778770300,
  "jti": "b6d0bc57-9efd-44ef-b25e-7be8396cb7c3",
  "sub": "user_12345",
  "email": "jane@example.com",
  "first_name": "Jane",
  "last_name": "Rivera",
  "accounts": [
    {
      "accountExternalId": "tenant_abc",
      "accountName": "Acme Dental",
      "accountDomain": "acme.example.com"
    }
  ]
}

Account fields:

  • accountId: Zentrik account UUID, if your team has stored it
  • accountExternalId: stable id from your product
  • accountName: display name for the customer account
  • accountDomain: optional domain associated with the account

If you cannot send account context on day one, Zentrik can still create a portal user from email. Account claims make reporting, vote coverage, and account-level ranking cleaner.

Additional claims are ignored unless this guide documents them. For example, an env claim is accepted but is not used to select the portal, choose an environment, or resolve identity.

Example endpoint

Example with Node and Express:

js40 lines
import crypto from 'node:crypto';
import jwt from 'jsonwebtoken';

app.get('/zentrik/ideas/login', requireSignedInUser, (req, res) => {
  const state = String(req.query.state || '');
  const returnTo = String(req.query.return_to || '/');

  const user = req.user;
  const now = Math.floor(Date.now() / 1000);

  const token = jwt.sign(
    {
      iat: now,
      exp: now + 5 * 60,
      jti: crypto.randomUUID(),
      sub: user.id,
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
      accounts: user.accounts.map((account) => ({
        accountExternalId: account.id,
        accountName: account.name,
        accountDomain: account.domain || undefined,
      })),
    },
    process.env.ZENTRIK_IDEAS_PORTAL_SHARED_SECRET,
    { algorithm: 'HS256' },
  );

  const callbackUrl = new URL(process.env.ZENTRIK_IDEAS_PORTAL_CALLBACK_URL);
  callbackUrl.searchParams.set('jwt', token);
  callbackUrl.searchParams.set('return_to', returnTo);

  // Portal-initiated login includes state; product-initiated login omits it.
  if (state) {
    callbackUrl.searchParams.set('state', state);
  }

  return res.redirect(callbackUrl.toString());
});

Store the shared secret server-side only. Never put it in browser JavaScript or mobile app code.

The configured callback URL must use your portal's alias-specific hostname so the same endpoint supports product-initiated requests that do not include state.

Account matching

Zentrik resolves account context in this order:

  • explicit account claims in accounts
  • legacy top-level account claims, if present
  • an existing portal user and linked account
  • an existing contact email in the workspace
  • a new portal account when no match exists

For production, prefer accounts[].accountExternalId plus accounts[].accountName. Those values let Zentrik keep vote and request history grouped even if display names change later.

Logout

When a user signs out from the Ideas Portal, Zentrik clears the portal session cookie and redirects the browser to your configured remote logout URL.

Your remote logout URL can either:

  • sign the user out of your product, then send them to your product login
  • keep the product session and show a confirmation page
  • redirect to a product route where they can choose what to do next

If your product logs the user out first and you also want to clear the Ideas Portal cookie, send the browser to:

http1 lines
GET https://acme.ideas.zentrik.ai/api/portal/portal_session/logout

That endpoint clears the Zentrik portal session and redirects the user back through the configured product login path.

Security checklist

Use this checklist before going live:

  • Keep the shared secret in server-side secret storage only.
  • Use HS256. Other algorithms are rejected.
  • Set iat to the current Unix timestamp in seconds.
  • Set exp to five minutes or less after iat.
  • Generate a new jti for every login. Reused jti values are rejected for replay protection.
  • For portal-initiated login, echo the exact state value from the remote login request.
  • For product-initiated login, omit state and use the alias-specific callback hostname.
  • Only generate tokens after your app has authenticated the user.
  • Use HTTPS for remote login and logout URLs.
  • Rotate the shared secret from Zentrik if it is ever exposed.

Zentrik accepts small clock differences, but large clock drift causes token validation to fail. Keep your application servers synced with NTP or an equivalent time source.

Testing

Test the full browser redirect flow, not only JWT signing.

Portal-initiated login

  1. Configure the remote login URL, remote logout URL, and shared secret in Settings → Portal.
  2. Open the portal URL in a fresh browser session.
  3. Confirm Zentrik redirects to your remote login URL with state and return_to.
  4. Sign in to your product if needed.
  5. Confirm your app redirects to the Zentrik callback with jwt, state, and return_to.
  6. Confirm the user lands in the Ideas Portal.
  7. Vote on an idea or submit a request.
  8. Sign out from the portal and confirm the remote logout path behaves as expected.

Product-initiated login on a shared browser

  1. Authenticate product user A and open your product launcher.
  2. Confirm your endpoint mints a fresh JWT and calls the alias-specific callback with jwt and return_to=%2Frequest, without state.
  3. Confirm the portal opens /request as user A.
  4. Sign out of your product only, authenticate product user B, and open the same launcher again.
  5. Confirm the successful callback replaces the existing portal session and the portal now shows user B.
  6. Retry the same JWT and confirm replay protection rejects it without changing user B's current portal session.

If your launcher opens the portal directly, repeat the shared-browser test with /request?refresh_identity=1 and confirm the final portal URL no longer contains refresh_identity.

If your Zentrik admin rotates the shared secret, update your product environment before testing again.

Troubleshooting

Check these steps against what you see in your workspace. If something differs, note your workspace name and the screen, then contact us.

The portal keeps redirecting to the product login page

For portal-initiated login, check that your callback redirect includes the same state value Zentrik sent to your remote login URL. For product-initiated login, omit state and confirm the callback uses the portal's alias-specific hostname. In both modes, confirm the user has a valid product session before your app signs the JWT.

Zentrik says authentication failed

Confirm the token is signed with HS256, the shared secret matches the current secret in Zentrik, and the required claims are present: iat, jti, email, first_name, and last_name.

A token worked once but fails when reused

That is expected. Zentrik treats jti as a replay-protection value. Generate a fresh jti and a fresh JWT for every login attempt.

The user lands on the wrong portal page

Pass through the original return_to query parameter from the remote login request. It should be a portal-relative path, not a full external URL.

A shared browser shows the previous product user

A valid portal_session is reused unless a new callback succeeds or the portal launch requests an identity refresh. For a product-owned button, use the product-initiated direct launch and mint a fresh JWT for the currently signed-in user. If the button opens the portal URL directly, append refresh_identity=1.

Votes show up without the expected account context

Add stable account claims in accounts, especially accountExternalId and accountName. Email-only login works, but account claims make reporting and coverage more reliable.

Need the portal URL, callback URL, or a new shared secret? Ask the workspace admin to open Settings → Portal in Zentrik.

Continue from here

Did this guide answer your question?

Your response helps us prioritize missing or unclear documentation.

Still stuck?

Send your question to Zentrik support. This guide will be included automatically.

Ask Zentrik support