Skip to content

Authentication

The ProxPanel API is JWT-secured. Every protected endpoint expects an Authorization: Bearer <token> header. This page documents how to obtain that token, what is inside it, and the defensive controls layered around the login endpoint.

https://your-panel-host/api

In SaaS mode the hostname determines the tenant. The path is identical (/api/auth/login) but the tenant context is resolved from the subdomain before the request reaches the handler.

A successful login returns two artifacts:

  1. A JWT access token in the JSON body. Short-lived (15 min by default). Sent on every subsequent request in the Authorization header.
  2. A refresh token as an HttpOnly cookie named refresh_token. Long-lived (30 days). Used only by POST /auth/refresh.

The access token is a signed HS256 JWT. Its claims are:

ClaimTypeDescription
user_idintPrimary key of the users row
usernamestringLogin name
user_typestringadmin, reseller, support, collector, customer
reseller_idint0 for admin; non-zero for any reseller-scoped user
tenant_idintSaaS only — non-zero in SaaS mode, 0 self-hosted
iatintIssued-at (unix seconds)
expintExpiry (unix seconds)

The signing secret is persisted in the system_preferences table on first boot so JWTs survive an API container restart. Treat the secret as sensitive — anyone who reads it can forge tokens for any user.

Authenticate with username + password and receive a JWT.

Permission required: none (public endpoint, rate-limited)

Request

POST /api/auth/login
Content-Type: application/json
FieldTypeRequiredDescription
usernamestringyesAdmin or reseller login name
passwordstringyesPlain password (sent over TLS only)
totp_codestringnoSix-digit TOTP code when 2FA is enabled on the account

Response — 200 OK

{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"user_type": "admin",
"is_active": true,
"permissions": ["*"]
}
}

The same response sets a cookie:

Set-Cookie: refresh_token=<opaque>; HttpOnly; Secure; SameSite=Lax; Path=/api/auth; Max-Age=2592000

Example

Terminal window
curl -X POST https://panel.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"hunter2"}'

Errors

StatusmessageCause
400username and password are requiredMissing field in body
401invalid credentialsUsername or password wrong
4012FA code requiredAccount has 2FA enabled, totp_code omitted
401invalid 2FA codeTOTP did not verify against the stored secret
403account is disabledusers.is_active = false, or the reseller record is suspended
403IP address not whitelistedCaller’s IP is outside the user’s allowed_ips CIDR list
423account locked, try again in N minutesBrute-force lockout active (see below)
429rate limit exceededMore than 10 login attempts per minute from one IP

Return the authenticated user, including the current permission set. Frontends call this on every page load so revoked permissions take effect on the next refresh without forcing a re-login.

Permission required: any authenticated user.

Request

GET /api/auth/me
Authorization: Bearer <jwt>

Response — 200 OK

{
"success": true,
"user": {
"id": 42,
"username": "reseller1",
"user_type": "reseller",
"reseller_id": 7,
"permissions": ["subscribers.view", "subscribers.create", "subscribers.renew"],
"reseller": { "id": 7, "name": "Reseller 1", "balance": 245.50 }
}
}

Errors

StatusmessageCause
401missing authorization headerNo Authorization header sent
401invalid tokenToken failed signature check, expired, or is blacklisted

Invalidate the current JWT. The token is added to the Redis blacklist for the remainder of its TTL. The refresh-token cookie is also cleared and its server-side row marked revoked.

Permission required: any authenticated user.

Request

POST /api/auth/logout
Authorization: Bearer <jwt>

Response — 200 OK

{ "success": true, "message": "logged out" }

After logout, the same JWT used again returns 401 invalid token. See JWT + Refresh Tokens for the blacklist mechanics.

Change the calling user’s password. Old password is required even for admins changing their own password.

Permission required: any authenticated user (changes only their own password).

Request

PUT /api/auth/password
Authorization: Bearer <jwt>
Content-Type: application/json
FieldTypeRequiredDescription
current_passwordstringyesExisting password
new_passwordstringyesMin 8 chars

Response — 200 OK

{ "success": true, "message": "password updated" }

Errors

StatusmessageCause
400new_password too shortLess than 8 characters
401current password incorrectOld password did not verify

POST /api/auth/change-password is an alias for the same handler kept for older clients.

Each users row has an optional allowed_ips field — a comma-separated list of CIDRs. When non-empty, the login handler rejects any attempt whose X-Real-IP (set by nginx) is outside every CIDR.

allowed_ips valueBehaviour
Empty / NULLNo restriction — login from any IP
203.0.113.5/32Only that exact IP
203.0.113.0/24,198.51.100.0/24Either subnet
0.0.0.0/0Explicit allow-all (same as empty)

The check fires before the password is even verified, so a leaked password from outside the allow-list still can’t be used.

The login handler tracks failed attempts per (username, ip) tuple in Redis. Thresholds:

Failed attempts in 1 hourActionDuration
5Account+IP locked15 minutes
10Account+IP locked1 hour
20Account+IP locked24 hours

On the response, the locked endpoint returns 423 Locked plus the remaining minutes. Successful login clears the counter.

The lockout is layered on top of the per-endpoint nginx rate limit (5 req/min on /api/auth/login) and the per-IP API rate limit (300 req/min global, 10 req/min for login). An attacker still needs to spread attempts across many IPs and many usernames — there is no global lockout that one bad actor could weaponise to lock real users out.

Endpoints under /api/auth/2fa/* manage per-user TOTP. See 2FA Setup for the UI walk-through. The API endpoints:

MethodPathPurpose
GET/api/auth/2fa/statusReturns { enabled: bool, qr_url: string }
POST/api/auth/2fa/setupGenerates a new secret + QR code (does not enable)
POST/api/auth/2fa/verifyVerifies a code and enables 2FA on success
POST/api/auth/2fa/disableDisables 2FA (requires a valid current TOTP code)

When 2FA is enabled, the /api/auth/login request must include totp_code or the response is 401 2FA code required.

All endpoints in this API surface return errors as:

{
"success": false,
"message": "human-readable reason"
}

The HTTP status code is the canonical signal. The message is for log lines and developer dashboards — do not write client code that parses it.

LimitScope
10 requests / min / IPPOST /api/auth/login (per-endpoint)
20 requests / min / IPPOST /api/auth/refresh (per-endpoint)
300 requests / min / IPAll other /api/* routes (global)
100 requests / min / IPExternal API key endpoints, see External API Keys

Rate limits return 429 Too Many Requests. Successful authentication does not exempt you from the global 300/min ceiling.