Single Sign-On (SSO)
Single Sign-On (SSO)
Log a customer straight into the panel from a trusted backend (WHMCS, billing, internal tools) without ever handling the panel password. The flow uses a single-use, server-side nonce β the session JWT is never placed in a URL.
Panel base: https://<your-server>:3082
When to use what
| You want to⦠| Endpoint |
|---|---|
| Log a customer in from WHMCS/billing | POST /api/v1/auth/sso/mint |
| Build a dashboard of N deep-link tiles in one call | POST /api/v1/auth/sso/mint-batch |
| (browser, automatic) redeem the link | GET /sso/consume/:nonce |
| (browser, automatic) hand the token to the SPA | POST /api/v1/auth/sso/exchange |
You only ever call mint. consume and exchange run in the
customer's browser β you just redirect to consume_url.
1. Mint a login link
Request β POST /api/v1/auth/sso/mint (auth = an API key;
caller must be admin or reseller):
POST /api/v1/auth/sso/mint
Authorization: Bearer wsp_β¦
Content-Type: application/json
{
"username": "john",
"target_path": "/dashboard",
"expires_in": 300,
"reason": "WHMCS SSO"
}
| Field | Required | Note |
|---|---|---|
username |
β | the panel user to log in (must not be admin/suspended) |
target_path |
β | panel-relative landing path; default / |
expires_in |
β | seconds, clamped to [30, 900], default 300 |
reason |
β | audit-log note |
Response 200 (verified live):
{
"nonce": "41NxfMurW1WCe80eamDZU8VLbjk_Mj5E5qbgs5UpH4I",
"consume_url": "https://161.248.4.182:3082/sso/consume/41NxfMurW1WCe80eamDZU8VLbjk_Mj5E5qbgs5UpH4I",
"expires_in": 300,
"target_path": "/dashboard"
}
Hand consume_url to the customer's browser (302 or a button).
There is no JWT yet β the session is only minted when the browser
hits consume_url.
Error responses (all verified live)
// 401 β no Authorization header
{ "success": false, "code": "UNAUTHORIZED",
"error": "Missing authorization", "message": "Missing authorization",
"status": 401 }
// 403 β authenticated but NOT with an API key (e.g. a plain admin JWT)
{ "success": false, "code": "FORBIDDEN",
"error": "Cross-system SSO mint requires API-key authentication",
"message": "β¦", "status": 403 }
// 403 β target is an admin account
{ "success": false, "code": "FORBIDDEN",
"error": "Cannot mint SSO for admin accounts",
"message": "Cannot mint SSO for admin accounts", "status": 403 }
// 400 β username missing
{ "success": false, "code": "VALIDATION_ERROR",
"error": "username is required",
"message": "username is required", "status": 400 }
Also 403 for a suspended target, or a reseller minting for a user
they don't own.
2. Mint many links at once (batch)
Request β POST /api/v1/auth/sso/mint-batch:
{ "username": "john",
"targets": ["/dashboard", "/files", "/databases", "/email"],
"expires_in": 300 }
targets = panel-relative paths, max 50.
Response 200 β one entry per target, order preserved:
{ "items": [
{ "nonce": "a1β¦", "consume_url": "https://β¦/sso/consume/a1β¦",
"expires_in": 300, "target_path": "/dashboard" },
{ "nonce": "b2β¦", "consume_url": "https://β¦/sso/consume/b2β¦",
"expires_in": 300, "target_path": "/files" } ] }
3. Consume (browser, automatic)
GET /sso/consume/:nonce β public, no auth header. The customer's
browser is redirected here. The panel atomically pops the nonce
(single-use), mints the session, and:
- Sets
wp_sso_tokenβ HttpOnly, Secure,SameSite=Strict, 5-min cookie carrying the JWT (never in the URL or page body). - Sets
wp_sso_pending=1β JS-readable flag for the SPA guard. - Issues an HTTP 302 to
target_path.
A bad / used / expired nonce renders an SSO error page instead.
Cross-system mints (API-key callers like WHMCS) produce a plain user token β not a login-as token β so a billing customer can never step up into admin context.
4. Exchange (browser, automatic)
POST /api/v1/auth/sso/exchange β public; the HttpOnly cookie IS
the credential. The SPA router guard calls this when it sees
wp_sso_pending=1. The panel reads wp_sso_token, wipes both SSO
cookies, and returns:
{ "token": "eyJhbGciOiJIUzI1Niβ¦" }
No cookie β 401 { "error": "No SSO cookie present" }.
End-to-end (WHMCS pattern)
WHMCS (holds API key, server-side)
β POST /api/v1/auth/sso/mint { username, target_path }
βΌ
WisPanel β { consume_url } (no JWT yet)
β 302 customer browser β consume_url
βΌ
GET /sso/consume/:nonce (public, single-use)
β Set-Cookie wp_sso_token (HttpOnly 5m) + wp_sso_pending=1
β 302 β target_path
βΌ
SPA boots, guard sees wp_sso_pending
β POST /api/v1/auth/sso/exchange (cookie = credential)
βΌ
{ token } β stored β customer is logged in
Notes
- TTL is clamped server-side to [30s, 900s] (default 300s).
target_pathis sanitised: must start with a single/, no scheme/host, no//, no CR/LF, β€ 200 chars β else falls back to/.- Every mint / consume is written to the panel audit log.