Auth
API: authentication and users
Common error shape for this handler is AUTH_ERR ({"error":"..."}), not always V1ERR.
Admin JWT claims used after AdminAuth(): user_id, username, role.
POST /api/v1/auth/login
Purpose. Authenticate an administrator and issue a JWT plus an HttpOnly session cookie.
Authentication. None. LoginRateLimiter() applies.
Parameters. None.
Request (authdomain.LoginRequest):
Scroll sideways to see the full table. Use the arrow keys when focused.
| Field | Type | Required |
|---|---|---|
username | string | yes (binding:"required") |
password | string | yes |
two_factor_code | string | when 2FA is enabled on the account (omitempty) |
Example request.
curl -sS -X POST https://manager.example.com:8443/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"YOUR_PASSWORD"}'Response (200). Handler JSON (note: domain LoginResponse.session_token is not placed in the body; cookie only):
{
"token": "<jwt>",
"expires_at": "2026-08-21T12:00:00Z",
"username": "admin",
"role": "admin"
}expires_at is time.Now().Add(config.GetTokenExpiry()). Cookie: HttpOnly session cookie issued only if cookiePolicy.ValidateIssuance accepts the request.
Errors.
Scroll sideways to see the full table. Use the arrow keys when focused.
| Status | Body | When |
|---|---|---|
| 400 | {"error":"Invalid request format"} | JSON bind failure |
| 400 | {"error":"Secure HTTPS session required"} | cookie policy rejects issuance |
| 401 | {"error":"Invalid credentials"} | unknown user, bad password, DB lookup failure |
| 401 | {"error":"Two-factor authentication code required","requires_2fa":true} | 2FA on, code omitted |
| 401 | {"error":"Invalid two-factor authentication code"} | TOTP/backup code fail |
| 403 | {"error":"Account is inactive"} | admin.Active == false |
| 500 | {"error":"Failed to generate token"} / Failed to generate session token / Failed to create session | |
| 429 | rate limiter |
Edition. SHARED.
Security. Do not log passwords. Prefer zwallctl login interactive prompt over --password. Default admin may be recreated during login if username matches configured default and no row exists. JWT is for API calls; session cookie is for browser bootstrap.
GET /api/v1/auth/status
Purpose. Browser session bootstrap. Validates the HttpOnly session cookie, not the JWT.
Authentication. None on the route; handler inspects the session cookie.
Parameters. None.
Request. Empty.
Example request.
curl -sS https://manager.example.com:8443/api/v1/auth/status \
--cookie 'session_token=COOKIE'Response (200).
{
"authenticated": true,
"session_id": 1,
"admin_id": 1,
"username": "admin",
"role": "admin",
"ip_address": "203.0.113.10"
}Errors. 401 {"authenticated":false,"message":"No active session found"} or "Invalid or expired session" / "Invalid session". Invalid cookies are cleared.
Edition. SHARED.
Security. Do not treat this as an unauthenticated user-directory. It only reports the cookie holder.
POST /api/v1/auth/js-tokens
Purpose. Issue short-lived random filenames for legacy console JavaScript assets (30-minute JS sessions).
Authentication. None on the route. Handler requires session cookie or X-Session-Token header.
Parameters. None.
Request. Empty body.
Example request. Cookie-authenticated browser call; not a typical zwallctl operation.
Response (200).
{
"tokens": {"app.js": "<random>", "security.js": "<random>"},
"expires_at": "2026-08-21T12:30:00Z"
}tokens is a map of original filename → random name. Generation failures for individual files are skipped.
Errors. 401 {"error":"No session token"}, {"error":"Invalid session"}, {"error":"Session expired"}.
Edition. SHARED.
Security. Not a public integration API. Tokens are not administrator JWTs.
GET /api/v1/auth/verify
Purpose. Return the authenticated admin identity from JWT/session context.
Authentication. AdminAuth().
Parameters. None.
Request. Empty.
Example request.
curl -sS https://manager.example.com:8443/api/v1/auth/verify \
-H "Authorization: Bearer TOKEN"Response (200).
{
"success": true,
"user": {"id": 1, "username": "admin", "role": "admin"}
}Errors. Middleware 401 if unauthenticated. Handler does not add its own bind errors.
Edition. SHARED.
POST /api/v1/auth/refresh
Purpose. Issue a new admin JWT from the current session-bound admin context.
Authentication. SessionAuth() (session cookie path, not a raw Bearer-only refresh).
Parameters. None.
Request. Empty body.
Example request. Browser session cookie to https://manager.example.com:8443/api/v1/auth/refresh.
Response (200).
{
"token": "<jwt>",
"expires_in": 28800
}expires_in is int(config.GetTokenExpiry().Seconds()).
Errors. 500 {"error":"Failed to refresh token"}.
Edition. SHARED.
POST /api/v1/auth/logout
Purpose. Blacklist a valid current JWT if present; deactivate the active session cookie. Idempotent cookie cleanup.
Authentication. Enterprise route: no gin auth middleware (router.go registers r.authHandler.Logout bare). Community route uses AdminAuth(). The handler still reads Authorization: Bearer and/or the session cookie / X-Session-Token.
Parameters. None.
Request. Empty body.
Example request.
curl -sS -X POST https://manager.example.com:8443/api/v1/auth/logout \
-H "Authorization: Bearer TOKEN"Response (200).
{"message": "Logged out successfully", "success": true}Returned even when neither JWT nor session is present (cookies still cleared).
Errors. 500 {"error":"Failed to complete logout"} if blacklist or session deactivation persistence fails. Invalid JWT does not block cookie cleanup.
Edition. SHARED (middleware difference as above).
Security. Send the JWT you want revoked. Stale JWT must not prevent cookie deletion.
POST /api/v1/auth/change-password
Purpose. Change the current administrator password.
Authentication. AdminAuth(). Username is taken from context (username), not from the body.
Parameters. None.
Request (anonymous bind struct on AuthHandler.ChangePassword):
Scroll sideways to see the full table. Use the arrow keys when focused.
| Field | Type | Required |
|---|---|---|
old_password | string | yes |
new_password | string | yes |
Example request.
{"old_password": "CURRENT", "new_password": "NEW_STRONG_PASSWORD"}Response (200).
{"success": true, "message": "Password changed successfully"}Errors.
Scroll sideways to see the full table. Use the arrow keys when focused.
| Status | Body |
|---|---|
| 400 | {"error":"Invalid request format"} |
| 400 | {"error":"<ValidatePasswordStrength message>"} |
| 400 | {"error":"Failed to change password. Please check your current password."} (wrong old password, missing user, or update failure) |
| 401 | {"error":"Unauthorized"} if username missing from context |
| 500 | same generic change-password error on hash failure |
Edition. SHARED.
Security. Do not log either password. Strength rules are authdomain.ValidatePasswordStrength.
POST /api/v1/auth/clear-js-sessions
Purpose. Clear JavaScript file-access sessions for the current browser session.
Authentication. SessionAuth().
Parameters. None.
Request. Empty. Session from cookie or X-Session-Token.
Response (200).
{"success": true, "message": "Sessions cleared successfully"}Errors. 401 No session token / Invalid session; 500 Failed to clear sessions.
Edition. SHARED.
GET /api/v1/auth/2fa/status
Purpose. Return whether 2FA is enabled for the current admin and remaining backup-code count.
Authentication. AdminAuth().
Parameters. None.
Request. Empty.
Example request. GET with Bearer JWT.
Response (200).
{
"data": {
"enabled": false,
"backup_codes_remaining": 8
}
}Errors. 401 {"error":"User not authenticated"}; 500 {"error":"Failed to load admin"}.
Edition. SHARED.
POST /api/v1/auth/2fa/setup
Purpose. Generate a TOTP secret, QR data URL, and eight backup codes; store the secret disabled until verify.
Authentication. AdminAuth(). User is user_id from context only.
Parameters. None.
Request. No request body. AuthHandler.Setup2FA does not call ShouldBindJSON. Domain type TwoFactorSetupRequest (password) is not read by this handler.
Example request.
curl -sS -X POST https://manager.example.com:8443/api/v1/auth/2fa/setup \
-H "Authorization: Bearer TOKEN"Response (200) (authdomain.TwoFactorSetupResponse):
{
"secret": "<base32>",
"qr_code": "data:image/png;base64,...",
"backup_codes": ["........", "........"]
}Errors. 401 {"error":"User not authenticated"}; 400 {"error":"Two-factor authentication is already enabled"}; 500 generation/store failures.
Edition. SHARED.
Security. Treat secret, qr_code, and backup_codes as secrets. Do not paste into tickets. Issuer label in TOTP is Zwall Centralized Security Manager.
POST /api/v1/auth/2fa/verify
Purpose. Confirm TOTP and enable 2FA.
Authentication. AdminAuth().
Request (TwoFactorVerifyRequest):
{"code": "123456"}code is required.
Response (200).
{"message": "Two-factor authentication enabled successfully"}Errors. 400 Invalid request format; 400 Invalid verification code; 401 User not authenticated; 500 enable failures.
Edition. SHARED.
POST /api/v1/auth/2fa/disable
Purpose. Disable 2FA after password + TOTP or backup code.
Authentication. AdminAuth().
Request (TwoFactorDisableRequest):
{"password": "CURRENT", "code": "123456"}Both fields required.
Response (200).
{"message": "Two-factor authentication disabled successfully"}Errors. 400 bind; 401 Invalid password or Invalid two-factor authentication code or User not authenticated; 500 disable failure.
Edition. SHARED.
POST /api/v1/auth/2fa/regenerate
Purpose. Replace TOTP secret and backup codes; keeps current enabled flag.
Authentication. AdminAuth(). No request body; uses user_id from context.
Response (200).
{
"secret": "<base32>",
"qr_code": "data:image/png;base64,...",
"backup_codes": ["........"]
}Errors. 401 Unauthorized; 500 generation/update failures.
Edition. SHARED.
Security. Operator-only. Regenerating secrets invalidates previous authenticator enrollments. Do not publish as a casual website cookbook.
GET /api/v1/auth/audit-logs
Purpose. List authentication/audit log rows for the console.
Authentication. AdminAuth().
Parameters (query):
Scroll sideways to see the full table. Use the arrow keys when focused.
| Param | Default | Notes |
|---|---|---|
limit | 20 | capped at 1000 |
offset | 0 | |
user | empty | filter by username |
Request. Empty body.
Response (200). Local AuditLogResponse array:
{
"logs": [
{
"id": "1",
"timestamp": "2026-08-21T12:00:00Z",
"user": "admin",
"action": "login_success",
"ip_address": "203.0.113.10",
"success": true,
"details": "",
"chain_version": 1,
"entry_hash": "<hash>"
}
],
"total": 1
}success is derived: false when action contains failed or error. total is len(responseData) for this page, not a separate count query.
Errors. 500 {"error":"Failed to load audit logs"}.
Edition. SHARED.
Users
GET /api/v1/users
Purpose. List administrator users.
Authentication. AdminAuth() + RoleAuth("admin").
Parameters. None.
Request. Empty.
Response (200).
{"users": [{"id": 1, "username": "admin", "role": "admin", "active": true, "two_factor_enabled": false, "created_at": "2026-01-01T00:00:00Z"}]}JSON fields from authdomain.Admin: id, username, email (omitempty), role, active, two_factor_enabled, created_at, last_login. Password hashes and 2FA secrets are json:"-".
Errors. 500 {"error":"Failed to list users"}.
Edition. SHARED. DELETE is the same router group as list/create.
Security. Admin-only. Not OIDC.
POST /api/v1/users
Purpose. Create an administrator user.
Authentication. AdminAuth() + RoleAuth("admin").
Request (anonymous struct):
Scroll sideways to see the full table. Use the arrow keys when focused.
| Field | Type | Required |
|---|---|---|
username | string | yes |
email | string | yes on bind; not stored on authdomain.User in this handler |
password | string | yes |
role | string | yes; normalized then validated |
active | bool | no; bind present but CreateUser builds User with username/password/role only |
Allowed roles after NormalizeAdminRole: admin, viewer, release_manager, campaign_operator.
Example request.
{
"username": "ops",
"email": "ops@example.com",
"password": "NEW_STRONG_PASSWORD",
"role": "admin",
"active": true
}Response (201).
{"success": true, "user": {"id": 2, "username": "ops", "role": "admin"}}Password is json:"-" on User.
Errors. 400 Invalid request format; 400 Invalid role. Must be 'admin', 'viewer', 'release_manager', or 'campaign_operator'; 400 password-strength message; 500 Failed to create user.
Edition. SHARED.
Security. Most operator APIs still require admin. Creating viewer does not grant fleet write.
DELETE /api/v1/users/:id
Purpose. Delete another administrator user.
Authentication. AdminAuth() + RoleAuth("admin").
Parameters. :id integer.
Request. Empty.
Response (200).
{"success": true, "message": "User deleted successfully"}Errors. 400 User ID is required / Invalid user ID format / Cannot delete your own user account; 500 Failed to delete user.
Edition. SHARED implementation; treat destructive admin user APIs as administrator-only.
Security. Self-delete is rejected using context user_id.