---
updatedAt: 2026-09-10T00:17:41.000Z
agentTools:
  projectIndex: https://developer.benjipays.com/llms.txt
---

# 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.

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:

```json
{
  "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

```javascript
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

```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.

```html
<!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`:

```bash
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:

```javascript
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](https://support.benjipays.com/support/solutions/articles/150000196355-customer-portal-sso-via-jwt).