Customer Portal SSO with a Custom JWT
Sign customers in to the Benji Pays Customer Portal with a short-lived HS256 JWT.
Custom JWT SSO is designed for applications that already authenticate their customers. Your server creates a short-lived token that identifies the signed-in customer, then submits it to the Benji Pays Customer Portal. Login can start on the Customer Portal login page or inside your own application.
The signing secret must remain on your server. Never mint these tokens in browser or mobile code.
Configure Benji Pays
In Benji Pays, open Settings → Customer Portal Settings → SSO Single Sign-On Configuration, add a configuration, and select JWT.
Set:
- Description — a name that administrators will recognize.
- Remote Login URL — your HTTPS application login endpoint. Benji Pays redirects customers here to authenticate and returns SSO errors here.
- Login Button Text — the text displayed on the Customer Portal login page.
Save the configuration, then securely record these generated values:
| Value | Purpose |
|---|---|
| Login Redirect URL | The Benji Pays callback URL, such as https://your-portal.example.com/callbackjwt?iss=your-issuer |
| ISSUER (ISS) | The required JWT iss claim |
| AUDIENCE | The required JWT aud claim; currently BENJIPAYS |
| CLIENTID | The required JWT client_id claim |
| CLIENTSECRET | The shared secret used to sign the JWT |
Store CLIENTSECRET in a server-side secret manager or environment variable. Do not commit it, send it to a browser, or write it to logs.
Login sequences
JWT SSO supports two entry points. Both end the same way: your application POSTs a freshly signed JWT to the Login Redirect URL from the customer's browser.
From the Customer Portal login page
Use this sequence when the customer arrives at the Benji Pays login page and clicks your SSO button.
- The customer clicks your SSO button on the Customer Portal login page.
- Benji Pays redirects the browser to your Remote Login URL.
- Your application authenticates the customer (or recognizes an existing session).
- Your server signs an HS256 JWT using
CLIENTSECRET. - Your application submits the JWT in a field named
tokento the Login Redirect URL. - Benji Pays validates the token, resolves portal access by email, and creates the Customer Portal session.
From your application
Use this sequence when the customer is already in your product — for example a "Pay invoices" or "Open Customer Portal" button, a deep link, or a server-rendered page that continues into Benji Pays. The Customer Portal SSO button is not required.
- The customer authenticates in your application, or is already signed in.
- The customer chooses to open the Customer Portal, or your application starts that step automatically.
- Your server signs an HS256 JWT using
CLIENTSECRET. - Your application returns an HTML page that POSTs the JWT in a field named
tokento the Login Redirect URL (the same auto-submit form as the portal-started flow). - Benji Pays validates the token, resolves portal access by email, and creates the Customer Portal session.
The POST must happen in the customer's browser against the exact portal Login Redirect URL so the session cookie is set on the Benji Pays Customer Portal origin. A server-to-server POST to /callbackjwt cannot transfer that session to the customer.
You can still keep a Remote Login URL: Benji Pays sends SSO errors there, and the portal SSO button (if shown) still redirects there. To hide the portal button and only start login from your application, turn off Show on Login Page in the SSO configuration.
Token requirements
Use the JWT header:
{
"alg": "HS256",
"typ": "JWT"
}Include these claims:
| Claim | Type | Requirement |
|---|---|---|
iss | string | Exact ISSUER (ISS) value from the SSO configuration |
aud | string | Exact AUDIENCE value; currently BENJIPAYS |
iat | number | Unix timestamp when the token was issued; no more than 30 seconds old when Benji Pays receives it |
exp | number | Unix timestamp after iat and no more than 5 minutes later |
email | string | Customer email, no more than 256 characters |
sub | string | Stable, unique user ID between 5 and 256 characters; it must not equal email |
client_id | string | Exact CLIENTID value from the SSO configuration |
emailVerified | boolean | Optional. Only literal true skips Benji Pays email verification |
Use an immutable internal user ID for sub, not an email address or another value that can be reassigned. Benji Pays scopes it to the issuer as jwt|{iss}|{sub}.
Only send emailVerified: true if your system has actually verified control of the asserted email address. A string such as "true" or a number such as 1 is not accepted as verification.
Create a token
These examples assume the SSO values are stored in server-side environment variables.
Node.js
const jwt = require('jsonwebtoken');
function createPortalToken(user) {
const issuedAt = Math.floor(Date.now() / 1000);
return jwt.sign(
{
iss: process.env.BENJI_SSO_ISSUER,
aud: 'BENJIPAYS',
iat: issuedAt,
exp: issuedAt + 30,
email: user.email,
sub: user.id,
client_id: process.env.BENJI_SSO_CLIENT_ID,
emailVerified: user.emailVerified === true
},
process.env.BENJI_SSO_CLIENT_SECRET,
{ algorithm: 'HS256' }
);
}Python
import os
import time
import jwt
def create_portal_token(user):
issued_at = int(time.time())
payload = {
"iss": os.environ["BENJI_SSO_ISSUER"],
"aud": "BENJIPAYS",
"iat": issued_at,
"exp": issued_at + 30,
"email": user["email"],
"sub": user["id"],
"client_id": os.environ["BENJI_SSO_CLIENT_ID"],
"emailVerified": user.get("email_verified") is True,
}
return jwt.encode(
payload,
os.environ["BENJI_SSO_CLIENT_SECRET"],
algorithm="HS256",
)Generate a fresh token immediately before submission. The narrow iat acceptance window makes queued or pre-generated tokens unreliable.
Submit the token
Both sequences use the same submission: return an HTML page that POSTs the token from the customer's browser to the exact Login Redirect URL shown in Benji Pays. That URL is bound to the company's Customer Portal domain; posting to a different host is rejected.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Continue to the Customer Portal</title>
</head>
<body>
<form id="benji-sso" method="post" action="https://YOUR_PORTAL_DOMAIN/callbackjwt?iss=YOUR_ISSUER">
<input type="hidden" name="token" value="YOUR_JWT">
<button type="submit">Continue to the Customer Portal</button>
</form>
<script>
document.getElementById('benji-sso').submit();
</script>
</body>
</html>When rendering this page, HTML-encode the token value. Do not interpolate untrusted data directly into HTML.
For server-side diagnostics, the callback also accepts the token as Authorization: Bearer:
curl -i -X POST "https://YOUR_PORTAL_DOMAIN/callbackjwt?iss=YOUR_ISSUER" \
-H "Authorization: Bearer ${BENJI_SSO_JWT}"The command verifies the callback response but does not transfer the resulting browser session to an end user. Use the form POST for the interactive login flow.
Handle login errors
Benji Pays can redirect the customer to the Remote Login URL with an error query parameter:
| Error | Meaning |
|---|---|
account-not-found | The email does not have access to an enabled Customer Portal company |
account-disabled | The matching account is disabled |
email-verification-required | The customer must complete Benji Pays email verification |
Display a safe, actionable message without reflecting the parameter itself into HTML:
const messages = {
'account-not-found': 'No Customer Portal account was found for this email.',
'account-disabled': 'This Customer Portal account is disabled.',
'email-verification-required': 'Verify your email address before continuing.'
};
const error = new URLSearchParams(window.location.search).get('error');
const message = messages[error];
if (message) {
document.querySelector('[data-sso-error]').textContent = message;
}Test the integration
- Start from the Customer Portal SSO button and confirm the customer lands in the portal.
- Start from a button or page in your application (no visit to the portal login page) and confirm the same result.
- Send
emailVerified: truefor an email your system verified and confirm no additional verification is requested. - Omit
emailVerifiedand confirm the verification path. - Test an unknown email and a disabled account.
- Confirm expired tokens, altered signatures, incorrect issuer/audience/client ID, and
sub === emailare rejected. - Confirm secrets and complete JWTs do not appear in client logs, analytics, or error monitoring.
For the corresponding settings walkthrough, see Customer Portal SSO via JWT.
Updated about 1 month ago
