Support Portal Customer Authentication

How customers sign in to your hosted support portal, and the exact contract your systems implement for each method. Sign-in is mandatory on the portal: it proves a visitor is actually YOUR customer, and every ticket is created and read under the verified identity from the session, never from client-supplied fields. The platform never stores customer passwords; you authenticate, the portal issues the session.

A portal has exactly ONE active sign-in method at a time, chosen in Dashboard → Support Tickets → Portal → Customer sign-in. Switching to a different method is one confirmed step.

MethodIdentity verified byIntegration needed
Username & passwordyour authentication endpointone HTTPS endpoint
Access tokenyour authentication endpointone HTTPS endpoint plus a deep link from your app
Email sign-in linksyour backend (via the REST API) or managed emailone REST call, or nothing at all
Third-party single sign-onyour SSO deployment or identity platformplatform-activated per organization; standard setup is a handful of fields

Username & password and access token (your authentication endpoint)link

The portal forwards what the customer presented to an HTTPS endpoint you host. Every request is BOTH signed and end-to-end encrypted with your shared secret:

  • x-newinstance-signature: sha256=<hex HMAC-SHA256(shared secret, raw body)> proves the request came from the platform.
  • credentials is an AES-256-GCM envelope over the sensitive fields, keyed by HKDF-SHA256 from the same shared secret, so the password or token is never readable in transit, in logs, or by any proxy between the platform and your handler.

Request (mode credentials):

{ "event": "customer.authenticate", "mode": "credentials",
  "username": "ada@example.com",
  "credentials": { "v": 1, "alg": "A256GCM", "kdf": "HKDF-SHA256",
                   "iv": "<b64>", "tag": "<b64>", "data": "<b64>" },
  "timestamp": 1765449600000 }

Mode token is identical with the token in the envelope instead of a username and password.

Decrypting the envelope (Node.js):

import { createDecipheriv, hkdfSync } from 'node:crypto';

function decryptCredentials(sharedSecret, env) {
  const key = Buffer.from(hkdfSync('sha256', sharedSecret,
    'newinstance-support-portal', 'credential-encryption-v1', 32));
  const d = createDecipheriv('aes-256-gcm', key, Buffer.from(env.iv, 'base64'));
  d.setAuthTag(Buffer.from(env.tag, 'base64'));
  return JSON.parse(Buffer.concat([d.update(Buffer.from(env.data, 'base64')), d.final()]).toString('utf8'));
}

Decryption yields { "password": "..." } (credentials mode) or { "token": "..." } (token mode).

Your response (HTTP 200):

{ "authenticated": true,
  "customer": { "id": "your-internal-id", "email": "ada@example.com", "name": "Ada L." } }

or

{ "authenticated": false, "message": "Optional reason shown to the customer" }

Rules: respond within 5 seconds. email is required on success (it links the customer to their ticket history); name falls back to the email; id is stored as the customer's external ID for correlation. Anything else (timeout, non-2xx, malformed body, missing email) fails closed and the customer is not signed in. Credential attempts are throttled per organization and username: 5 failures cause a 15-minute lockout. Use the Test endpoint action in the dashboard to probe your endpoint with a signed customer.authenticate_test event before going live.

Optional: browser-side RSA encryption (username & password)link

For zero platform visibility of passwords, paste an RSA public key (SPKI PEM) plus a key ID in the dashboard. The customer's browser then encrypts the credential payload with RSA-OAEP-SHA-256 before it leaves the page, and the platform forwards the envelope verbatim; only you can decrypt it:

{ "event": "customer.authenticate", "mode": "credentials",
  "username": "ada@example.com",
  "encryption": "rsa-oaep-256",
  "credentials": { "v": 2, "alg": "RSA-OAEP-256", "kid": "key-2026-08", "data": "<b64>" },
  "timestamp": 1765449600000 }

Decrypt data with your private key (RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256') to get { "password": "...", "nonce": "<uuid>", "ts": <unix ms> }. Reject replays by enforcing nonce uniqueness and a tight timestamp window; the platform already rejects envelopes older than 5 minutes. Rotate by saving a new key with a new key ID; the previous key keeps validating in-flight envelopes until you rotate again.

Two ways to mint, zero endpoints to host:

  1. Your backend (which already has its own session for the customer) mints a single-use link through Support Tickets → Mint sign-in link in this collection and redirects the customer to it. Your org API key is the authentication.
  2. Self-service: the customer types their email on the portal login page and the platform emails them a link through your configured email channel. Requests are rate-limited (3 per email per 15 minutes) and always answered neutrally, so there is no account enumeration.

Links are single-use, expire after 10 minutes, and only their SHA-256 is stored server-side.

Third-party single sign-onlink

Platform-activated per organization. The standard setup is a handful of fields (SSO base URL, public key, secret key, salt); the sign-in page, callback, token verification, profile retrieval, session management, and logout are all derived and handled by the portal. A custom setup style exists for identity platforms with their own contract: you describe the verification request (URL, method, token placement, static parameters) and map your profile fields (any names, for example sub or mail) onto the support identity with dot paths. Either way, your platform keeps owning authentication and revoking a session upstream ends the portal session at its next re-check.

The session JWT the portal issueslink

After any successful sign-in the portal mints an HS256 JWT and keeps it in an httpOnly cookie:

{ "sub": "<platform customer id>", "org": "<organization id>",
  "email": "ada@example.com", "name": "Ada L.",
  "provider": "delegated | sso | email_link | external",
  "method": "credentials | token | link | sso | email_link | external",
  "extId": "<your stable customer id, when known>",
  "jti": "...", "iat": 0, "exp": 0 }

It is signed with your organization's customer-session JWT secret (shown in the dashboard, copyable), so your backend can independently verify "this customer is interacting, and they are authenticated" with any HS256 JWT library. Sessions last 30 days; disabling a customer cuts access on their next request, and rotating the signing secret invalidates every live session at once.