JWT Authentication
Identify users in your help widget without requiring a separate login. When users are logged into your app, you can pass their identity to Ferndesk using a JWT token signed by your backend.
You'll need your JWT secret from Help Center > Access Control and the Ferndesk SDK installed.
JWT identify is available on all plans and works in both Public and Private access modes. You do not need a private help center or a plan upgrade to generate a secret.
How It Works
Three-step flow:
Your frontend detects a logged-in user
Your backend generates a signed JWT with user details
Your frontend calls
Ferndesk('identify', { jwt })
The help center and widget now knows who the user is for authentication, personalization and analytics.
JWT identify accepts a JWT from any HTTPS origin. Subsequent widget-session requests are bound to the exact origin that submitted the JWT. The widget and help center must be served from the exact same origin, including scheme, host, and port.
JWT Secret
Before you can sign JWTs, you need a signing secret from your Ferndesk dashboard:
Go to Help Center > Access Control.
Open the Access tab.
In the Sign in with your product section, under Single sign-on, find the Signing secret area.
Click Generate Secret.
The section shows a status: Not set up means no secret exists yet. Identify only means the secret works for silent identify, but readers cannot sign in through your product yet because no valid Login URL is configured. Enabled means the secret and a valid Login URL are both in place.
The secret is masked after generation. Copy it immediately and store it securely in your backend environment variables. You can reveal it again later from the dashboard.
Silent identification can also be done by sending a JWT signed with the same secret to the identify endpoint /api/auth/identify.
Only active workspace owners and admins can generate, reveal, rotate, or delete the secret. Other workspace members see guidance to ask an owner or admin instead of the secret-management controls.
If you need to rotate the secret, click Regenerate and confirm the rotation in the Regenerate the JWT signing secret? dialog. The existing secret is replaced and all tokens signed with the old one stop working immediately. Deploy the new secret to your backend before help center sign-in resumes.
You can only have one JWT secret per help center. Generate Secret is only available when no secret exists. Regenerate is available when a secret already exists.
Sign in with your product
Readers sign in through the help center's own sign-in page using the methods the help center enables. One of those methods is sign-in through your product: the help center redirects the reader to your product, your backend signs a short-lived JWT, and the reader returns to the help center signed in. The same signing secret also supports silent identification from inside your app.
AI clients that read your help center through MCP sign in with the help center's own sign-in methods in the browser.
Setup steps are in Sign in with your product.
Related: Use Ferndesk with AI tools · Let readers use your help center in AI tools
Generate the JWT Server-Side
Create an endpoint that returns a signed token. Ferndesk does not require iss (issuer) or aud (audience) claims.
Required claims:
sub(string, required): Unique ID in your system that identifies this user. This is the primary identity key.email(string, required): User's email addressexp(number, required): Token expiration timestampiat(number, required): Token issuance timestamp
Optional claims:
name(string, optional): Display namecustomAttributes(object, optional): Extra user attributes. You can also usemetadata; both keys are accepted.
The sub claim must remain stable for each user. Ferndesk uses this subject to identify and link users. If a user's sub changes, they will not be linked to their previous help center identity.
Node.js example:
const jwt = require('jsonwebtoken');
app.get('/api/ferndesk-token', async (req, res) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign({
sub: req.user.id,
email: req.user.email,
name: req.user.name,
iat: now,
exp: now + 900, // 15 minutes (maximum token age)
customAttributes: { plan: req.user.plan }
}, process.env.FERNDESK_JWT_SECRET, { algorithm: 'HS256' });
res.send(token);
});Python example:
import jwt
import os\nimport time
@app.route('/api/ferndesk-token')
def ferndesk_token():
if not current_user:
return {'error': 'Not authenticated'}, 401
now = int(time.time())
token = jwt.encode({
'sub': current_user.id,
'email': current_user.email,
'name': current_user.name,
'iat': now,
'exp': now + 900, # 15 minutes (maximum token age)
'customAttributes': {'plan': current_user.plan}
}, os.environ['FERNDESK_JWT_SECRET'], algorithm='HS256')
return tokenNever expose your JWT secret in client-side code. Store it in environment variables server-side only.
Call Identify from Your Frontend
Fetch the token from your backend and pass it to the SDK:
Ferndesk('init', { widgetId: 'your-widget-id' });
fetch('/api/ferndesk-token').then(r => r.text())
.then(jwt => Ferndesk('identify', { jwt }))
.catch(err => console.error('Identification failed:', err));React example:
useEffect(() => {
window.Ferndesk('init', { widgetId: 'your-widget-id' });
if (currentUser) {
fetch('/api/ferndesk-token')
.then(r => r.text())
.then(jwt => window.Ferndesk('identify', { jwt }));
}
}, [currentUser]);Call identify after init but before opening the widget. To log out, call POST /api/auth/logout. This clears the Help Center cookie session only: it does not clear the widget's in-memory identity or its stored widget session token. To reset the widget's identity, destroy the widget with Ferndesk('destroy') and initialize it again.
Verify It's Working
Check these indicators:
Browser console: No errors. Invalid JWTs show
Ferndesk: identify failed (Invalid or expired token).Contact form: Email and name will be pre-filled
Analytics: User sessions appear in your dashboard
Common Errors
Ferndesk: identify requires a jwt
Missing jwt parameter. Check that your backend is returning a JWT string.
invalid jwt
Signature verification or claim validation failed. Verify:
Correct JWT secret matches what's stored in Ferndesk
Token hasn't expired
Algorithm is HS256, HS384, or HS512 (the examples above use HS256)
Required claims are present:
sub,email,exp, andiat
JWT subject does not match the existing help-center user
This error occurs when a user's email already exists in your help center but with a different subject ID. This means someone previously signed in with that email using a different identity system or sub value.
To resolve:
Ensure your backend always sends the same
subfor each userIf you've changed user ID systems, the affected user will need to be re-provisioned in Ferndesk
must be called from same domain or 1-level subdomain
Origin mismatch. Subsequent widget-session requests must come from the exact origin that performed identification, including scheme, host, and port.
The sub claim is the primary identifier for users. While email is required, Ferndesk binds identity to the subject, not the email address alone. This prevents account takeover if email addresses change or are reused.
Security Notes
Set token expiration to 15 minutes or less
Only generate tokens for authenticated users
Use HTTPS everywhere
Never commit secrets to version control
Keep your
subvalues stable. Ferndesk persists identity by subject.