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.
Base URL
Section titled “Base URL”https://your-panel-host/apiIn 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.
Token model
Section titled “Token model”A successful login returns two artifacts:
- A JWT access token in the JSON body. Short-lived (15 min by default). Sent on every subsequent request in the
Authorizationheader. - A refresh token as an
HttpOnlycookie namedrefresh_token. Long-lived (30 days). Used only byPOST /auth/refresh.
The access token is a signed HS256 JWT. Its claims are:
| Claim | Type | Description |
|---|---|---|
user_id | int | Primary key of the users row |
username | string | Login name |
user_type | string | admin, reseller, support, collector, customer |
reseller_id | int | 0 for admin; non-zero for any reseller-scoped user |
tenant_id | int | SaaS only — non-zero in SaaS mode, 0 self-hosted |
iat | int | Issued-at (unix seconds) |
exp | int | Expiry (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.
POST /api/auth/login
Section titled “POST /api/auth/login”Authenticate with username + password and receive a JWT.
Permission required: none (public endpoint, rate-limited)
Request
POST /api/auth/loginContent-Type: application/json| Field | Type | Required | Description |
|---|---|---|---|
username | string | yes | Admin or reseller login name |
password | string | yes | Plain password (sent over TLS only) |
totp_code | string | no | Six-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=2592000Example
curl -X POST https://panel.example.com/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"hunter2"}'Errors
| Status | message | Cause |
|---|---|---|
| 400 | username and password are required | Missing field in body |
| 401 | invalid credentials | Username or password wrong |
| 401 | 2FA code required | Account has 2FA enabled, totp_code omitted |
| 401 | invalid 2FA code | TOTP did not verify against the stored secret |
| 403 | account is disabled | users.is_active = false, or the reseller record is suspended |
| 403 | IP address not whitelisted | Caller’s IP is outside the user’s allowed_ips CIDR list |
| 423 | account locked, try again in N minutes | Brute-force lockout active (see below) |
| 429 | rate limit exceeded | More than 10 login attempts per minute from one IP |
GET /api/auth/me
Section titled “GET /api/auth/me”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/meAuthorization: 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
| Status | message | Cause |
|---|---|---|
| 401 | missing authorization header | No Authorization header sent |
| 401 | invalid token | Token failed signature check, expired, or is blacklisted |
POST /api/auth/logout
Section titled “POST /api/auth/logout”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/logoutAuthorization: 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.
PUT /api/auth/password
Section titled “PUT /api/auth/password”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/passwordAuthorization: Bearer <jwt>Content-Type: application/json| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | yes | Existing password |
new_password | string | yes | Min 8 chars |
Response — 200 OK
{ "success": true, "message": "password updated" }Errors
| Status | message | Cause |
|---|---|---|
| 400 | new_password too short | Less than 8 characters |
| 401 | current password incorrect | Old password did not verify |
POST /api/auth/change-password is an alias for the same handler kept for older clients.
IP whitelist enforcement
Section titled “IP whitelist enforcement”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 value | Behaviour |
|---|---|
| Empty / NULL | No restriction — login from any IP |
203.0.113.5/32 | Only that exact IP |
203.0.113.0/24,198.51.100.0/24 | Either subnet |
0.0.0.0/0 | Explicit 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.
Brute-force lockout
Section titled “Brute-force lockout”The login handler tracks failed attempts per (username, ip) tuple in Redis. Thresholds:
| Failed attempts in 1 hour | Action | Duration |
|---|---|---|
| 5 | Account+IP locked | 15 minutes |
| 10 | Account+IP locked | 1 hour |
| 20 | Account+IP locked | 24 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.
Two-factor authentication (TOTP)
Section titled “Two-factor authentication (TOTP)”Endpoints under /api/auth/2fa/* manage per-user TOTP. See 2FA Setup for the UI walk-through. The API endpoints:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/auth/2fa/status | Returns { enabled: bool, qr_url: string } |
| POST | /api/auth/2fa/setup | Generates a new secret + QR code (does not enable) |
| POST | /api/auth/2fa/verify | Verifies a code and enables 2FA on success |
| POST | /api/auth/2fa/disable | Disables 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.
Error format
Section titled “Error format”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.
Rate limits
Section titled “Rate limits”| Limit | Scope |
|---|---|
| 10 requests / min / IP | POST /api/auth/login (per-endpoint) |
| 20 requests / min / IP | POST /api/auth/refresh (per-endpoint) |
| 300 requests / min / IP | All other /api/* routes (global) |
| 100 requests / min / IP | External 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.
Related pages
Section titled “Related pages”- JWT + Refresh Tokens — token lifecycle, rotation, replay detection
- External API Keys — long-lived keys for integrations
- Endpoints — Subscribers — first protected resource to call after login
- Permissions — how
permissionsin the/auth/meresponse are assigned