Single sign-on
Single sign-on lets users sign in to your public UserJot board through your own authentication system. When users click Sign In, UserJot sends them to your SSO endpoint. After you authenticate them, you send them back to UserJot with a signed JWT.
This is for public board login. It is separate from automatic login, which carries widget-identified users into public UserJot surfaces from your app.
Enable SSO
- Open Settings in your workspace.
- Go to Login.
- Under Authentication, set Board login to SSO. The first time you do this, an Enable SSO dialog asks for your redirect URL.
- Enter your SSO redirect URL and confirm with Enable SSO. You can change it later from the SSO redirect URL section on the same page.
- Copy your Project ID and Project secret from the Secrets section.
Your SSO redirect URL must use https://. During local development, use an
HTTPS tunnel if you need to test the full redirect flow.
Board login modes require the Professional plan.
How the flow works
When a user starts SSO, UserJot redirects their browser to your configured SSO
Redirect URL. The only query parameter your endpoint needs is state:
https://yourapp.com/userjot/sso?state=OPAQUE_STATE_FROM_USERJOTYour endpoint should:
- Read the
statequery parameter. - Authenticate the user in your app.
- Create a JWT signed with your UserJot Project secret.
- Redirect to
https://api.userjot.com/auth/ssowithjwtand the originalstate.
Do not modify the state value. UserJot uses it to verify that the login was
started by the same browser session. The state value is one-time use and expires
after 15 minutes.
If the user is not signed in yet, send them through your normal login flow and then return them to this same SSO endpoint with the original query parameters.
JWT payload
The JWT must be signed with HS256 using your UserJot Project secret.
At minimum, send the user ID, issuer, audience, and expiration. You can also include an email address, profile fields, custom traits, and company memberships.
| Claim | Required | Description |
|---|---|---|
sub | Yes | Stable user ID from your system |
email | No | User's email address |
iss | Yes | Exact UserJot Project ID |
aud | Yes | Exact string userjot |
exp | Yes | Unix expiration timestamp |
iat | No | Unix issued-at timestamp |
firstName | No | Given profile field |
lastName | No | Family profile field |
avatar | No | Avatar URL |
traits | No | Custom user traits |
companies | No | Company or account memberships |
Example payload:
const now = Math.floor(Date.now() / 1000);
const payload = {
sub: 'user_123',
email: 'jane@example.com',
firstName: 'Jane',
lastName: 'Doe',
iss: 'YOUR_PROJECT_ID',
aud: 'userjot',
iat: now,
exp: now + 10 * 60
};Use a short expiration window. Five to fifteen minutes is a good default. If you
include iat, it must not be in the future beyond normal clock skew.
If email is omitted, UserJot identifies the user by sub and creates or
updates a member without an email address. Email-based linking and email
notifications only become available after an email address is provided.
Trait values must be JSON scalars: strings, numbers, booleans, or null.
Nested objects and arrays are ignored.
Example endpoint
import { Hono } from 'hono';
import jwt from 'jsonwebtoken';
const app = new Hono();
const USERJOT_PROJECT_ID = 'YOUR_PROJECT_ID';
const USERJOT_PROJECT_SECRET = 'YOUR_PROJECT_SECRET';
const USERJOT_CALLBACK_URL = 'https://api.userjot.com/auth/sso';
app.get('/userjot/sso', async (c) => {
const state = c.req.query('state');
if (!state) {
return c.text('Missing state', 400);
}
// Authenticate the user however your app normally does.
const user = await getCurrentUser(c);
const payload = {
sub: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
iss: USERJOT_PROJECT_ID,
aud: 'userjot'
};
const token = jwt.sign(payload, USERJOT_PROJECT_SECRET, {
algorithm: 'HS256',
expiresIn: '10m'
});
const callback = new URL(USERJOT_CALLBACK_URL);
callback.searchParams.set('jwt', token);
callback.searchParams.set('state', state);
return c.redirect(callback.toString());
});Keep the Project secret on your server. Never expose it in browser code.
Company data
If your product is account-based, you can include company memberships in the same JWT payload:
{
"companies": [
{
"id": "company_456",
"domain": "acme.com",
"logo": "https://example.com/acme.png",
"traits": {
"plan": "enterprise",
"seats": 42
}
}
]
}Use id for the stable account ID from your system. You can skip company data
unless it is useful for your workspace.
Troubleshooting
Users are sent back without being logged in
Check that:
- the JWT is signed with the current Project secret
issmatches the exact Project ID from your workspace settingsaudis the exact stringuserjotsubandexpare present- your endpoint sends back the exact
statevalue it received
If SSO fails after the callback, UserJot redirects back to the board with an
sso_error query parameter such as invalid_state, invalid_token,
auth_failed, or session_error.
The login button does not use SSO
Check that Board login is set to
SSO in Settings → Login.
You changed the Project secret
After rotating the Project secret, update your SSO endpoint immediately. Tokens signed with the old secret will no longer work.