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.
GET https://app.example.com/zentrik/ideas/login?state=RANDOM_STATE&return_to=%2FYour endpoint must:
- require the user to be signed in to your app
- preserve the exact
statequery parameter - preserve the
return_tovalue 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 secondsjti: unique token id; generate a fresh value for every login attemptemail: signed-in user's email addressfirst_name: signed-in user's first namelast_name: signed-in user's last name
Recommended claims:
exp: expiration time, usually five minutes or less afteriatsub: stable user id from your productaccounts: 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 tokenstate: the exact state value Zentrik sent to your remote login URLreturn_to: the same portal path, if present
GET https://acme.ideas.zentrik.ai/api/portal/auth/jwt/callback?jwt=TOKEN&state=RANDOM_STATE&return_to=%2FThe 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_toor 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:
{
"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:
{
"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 itaccountExternalId: stable id from your productaccountName: display name for the customer accountaccountDomain: 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:
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:
GET https://acme.ideas.zentrik.ai/api/portal/portal_session/logoutThat 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
iatto the current Unix timestamp in seconds. - Set
expto five minutes or less afteriat. - Generate a new
jtifor every login. Reusedjtivalues are rejected for replay protection. - Echo the exact
statevalue 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.
- Configure the remote login URL, remote logout URL, and shared secret in Settings → Portal.
- Open the portal URL in a fresh browser session.
- Confirm Zentrik redirects to your remote login URL with
stateandreturn_to. - Sign in to your product if needed.
- Confirm your app redirects to the Zentrik callback with
jwt,state, andreturn_to. - Confirm the user lands in the Ideas Portal.
- Vote on an idea or submit a request.
- 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.