Single sign-on with JWT
With JWT single sign-on your application authenticates the user, then redirects them to Deeper Signals with a short-lived signed token. Deeper Signals finds the user by ds_id or e-mail, creates the account if it does not exist yet, signs them in and, optionally, runs a playbook that assigns assessments, groups and projects.
The SSO URL
https://app.deepersignals.com/sso/{jwt}?error_url=https://your-app.example.com/sso-error
| Part | Description |
|---|---|
{jwt} | The signed token described below, URL-safe (standard compact JWT encoding). |
error_url (optional) | Where to send the user if the token is invalid or expired. The domain must be one of the redirect domains registered for your API token. |
Building the token
Sign the JWT with HMAC SHA-256 (HS256) using your account API token as the secret. Use the token string as-is; do not base64-encode it first.
Generate the token on your server, never in client-side JavaScript, so the API token is not exposed.
Claims
| Claim | Required | Description |
|---|---|---|
iat | yes | Issued-at time, seconds since the UNIX epoch. |
exp | yes | Expiry, seconds since the UNIX epoch. Tokens are valid for at most 15 minutes after iat; a shorter window is fine. |
client_id | yes | The client id that was issued together with your API token. |
email | yes | The user's e-mail. Used as the unique identifier when ds_id is absent. |
first_name | yes | The user's first name. |
last_name | yes | The user's last name. |
ds_id | no | The user's Deeper Signals id, if you stored it from the User Created webhook or an API response. Takes precedence over email for the lookup. |
locale | no | Two-letter language code. Defaults to en; unsupported languages fall back to English. |
identity | no | The playbook identity to run after sign-in: assigns assessments, groups and projects. |
Example payload
{
"iat": 1760000000,
"exp": 1760000600,
"client_id": "6hUsd268",
"email": "user@example.com",
"first_name": "Jane",
"last_name": "Doe",
"ds_id": 15768,
"locale": "en",
"identity": "60ae0168f2eb7"
}
Code samples
- Node.js
- Python
- PHP
import jwt from 'jsonwebtoken';
const token = jwt.sign(
{
client_id: process.env.DS_CLIENT_ID,
email: user.email,
first_name: user.firstName,
last_name: user.lastName,
locale: 'en',
identity: 'YOUR_PLAYBOOK_IDENTITY',
},
process.env.DS_API_TOKEN,
{ algorithm: 'HS256', expiresIn: '10m' } // sets iat and exp
);
res.redirect(`https://app.deepersignals.com/sso/${token}?error_url=${encodeURIComponent('https://your-app.example.com/sso-error')}`);
import time, jwt # PyJWT
now = int(time.time())
token = jwt.encode(
{
"iat": now,
"exp": now + 600,
"client_id": DS_CLIENT_ID,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"locale": "en",
"identity": "YOUR_PLAYBOOK_IDENTITY",
},
DS_API_TOKEN,
algorithm="HS256",
)
return redirect(f"https://app.deepersignals.com/sso/{token}")
use Firebase\JWT\JWT;
$now = time();
$token = JWT::encode([
'iat' => $now,
'exp' => $now + 600,
'client_id' => $_ENV['DS_CLIENT_ID'],
'email' => $user->email,
'first_name' => $user->firstName,
'last_name' => $user->lastName,
'locale' => 'en',
'identity' => 'YOUR_PLAYBOOK_IDENTITY',
], $_ENV['DS_API_TOKEN'], 'HS256');
header('Location: https://app.deepersignals.com/sso/' . $token);
What happens on the platform
- The signature,
expandclient_idare verified. Failures redirect toerror_urlwhen given. - The user is looked up by
ds_id, otherwise byemail, within your account. - A missing user is created with the names and locale from the token.
- If
identitynames a playbook, its assessments, groups and projects are applied. - The user lands signed in on the platform.
Troubleshooting
| Symptom | Likely cause |
|---|---|
Redirected to error_url immediately | Signature mismatch (wrong secret, base64-encoded secret, wrong algorithm) or client_id not matching the token. |
| Works in tests, fails in production | Clock skew: iat in the future or exp more than 15 minutes ahead. Sync your server clock. |
| A second user is created | email differs in case or spelling from the existing user. Send ds_id once you have it. |
| User signed in but no assessment assigned | identity missing or not a playbook of this account. Check GET /playbook. |