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:

ValuePurpose
Login Redirect URLThe Benji Pays callback URL, such as https://your-portal.example.com/callbackjwt?iss=your-issuer
ISSUER (ISS)The required JWT iss claim
AUDIENCEThe required JWT aud claim; currently BENJIPAYS
CLIENTIDThe required JWT client_id claim
CLIENTSECRETThe 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.

  1. The customer clicks your SSO button on the Customer Portal login page.
  2. Benji Pays redirects the browser to your Remote Login URL.
  3. Your application authenticates the customer (or recognizes an existing session).
  4. Your server signs an HS256 JWT using CLIENTSECRET.
  5. Your application submits the JWT in a field named token to the Login Redirect URL.
  6. 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.

  1. The customer authenticates in your application, or is already signed in.
  2. The customer chooses to open the Customer Portal, or your application starts that step automatically.
  3. Your server signs an HS256 JWT using CLIENTSECRET.
  4. Your application returns an HTML page that POSTs the JWT in a field named token to the Login Redirect URL (the same auto-submit form as the portal-started flow).
  5. 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:

ClaimTypeRequirement
issstringExact ISSUER (ISS) value from the SSO configuration
audstringExact AUDIENCE value; currently BENJIPAYS
iatnumberUnix timestamp when the token was issued; no more than 30 seconds old when Benji Pays receives it
expnumberUnix timestamp after iat and no more than 5 minutes later
emailstringCustomer email, no more than 256 characters
substringStable, unique user ID between 5 and 256 characters; it must not equal email
client_idstringExact CLIENTID value from the SSO configuration
emailVerifiedbooleanOptional. 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:

ErrorMeaning
account-not-foundThe email does not have access to an enabled Customer Portal company
account-disabledThe matching account is disabled
email-verification-requiredThe 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: true for an email your system verified and confirm no additional verification is requested.
  • Omit emailVerified and confirm the verification path.
  • Test an unknown email and a disabled account.
  • Confirm expired tokens, altered signatures, incorrect issuer/audience/client ID, and sub === email are 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.


Did this page help you?