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 receives a login request from the portal, signs a short-lived JWT, and sends the user back 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.

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

Login flow

The login starts at the Ideas Portal. Do not generate a JWT before Zentrik redirects to your remote login URL, because the 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.

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.

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 || '/');

  if (!state) {
    return res.status(400).send('Missing state');
  }

  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('state', state);
  callbackUrl.searchParams.set('return_to', returnTo);

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

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

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.
  • Echo the exact state value from the remote login request.
  • 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.

  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.

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

Troubleshooting

These items are about workflow and expectations in the product, not a broken OAuth client. If something contradicts what you see in your workspace, note your workspace name and the screen, then contact us.

The portal keeps redirecting to the product login page

Check that your callback redirect includes the same state value Zentrik sent to your remote login URL. Also 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.

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.