Authentication
Proxima Console uses a multi-mode authentication system supporting JWT tokens and service account API keys. This page covers all authentication and user management flows.
Authentication Modes
The backend supports two authentication modes on all /api/v1/* routes:
| Mode | Header | Use Case |
|---|---|---|
| JWT Bearer | Authorization: Bearer <jwt> | User login sessions (frontend, API clients) |
| API Key | X-API-Key: <key> | Service accounts with scoped permissions |
Both modes resolve full permission contexts (per-client RBAC).
Login Flow
POST /api/v1/auth/login
Request:
{
"email": "[email protected]",
"password": "securepassword123"
}
Flow:
- Check progressive lockout (5 failures: 1min, 10: 5min, 15: 15min, 20+: 1h)
- Look up user by email (same 401 for wrong email or wrong password to prevent enumeration)
- Verify account status is
active - Verify password (bcrypt)
- If MFA is enabled, return
202 Acceptedwith an MFA challenge token - Issue JWT access token (15min TTL) + refresh token (7-day TTL)
- Create session record with device label and IP address
- Record audit log entry
Success Response (no MFA): 200 OK
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "base64url-encoded-random-bytes",
"session_id": "session-uuid",
"token_type": "Bearer",
"expires_in": 900
}
}
The session_id is included so the frontend can identify the current session in the session list (e.g., to show a "Current" badge and confirm before self-revocation).
MFA Required Response: 202 Accepted
{
"data": {
"mfa_required": true,
"mfa_token": "uuid-challenge-token"
}
}
Rate Limiting
Public auth endpoints (login, refresh, forgot-password, reset-password, accept-invite) are rate-limited to 5 requests per minute per IP by default, separate from the general API rate limit of 1000/min. Protected auth endpoints (sessions, me, mfa, password change) use only the general API rate limit.
Both limits are configurable via environment variables:
| Variable | Default | Description |
|---|---|---|
PROXIMA_API_RATE_LIMIT | 1000 | General API rate limit (requests/min/IP) |
PROXIMA_AUTH_RATE_LIMIT | 5 | Public auth rate limit (requests/min/IP) |
MFA (Two-Factor Authentication)
Proxima Console supports TOTP-based two-factor authentication with recovery codes. MFA is mandatory — users who have not enabled MFA are redirected to the MFA setup page (/setup-mfa) on any attempt to access a protected route. This ensures all users complete MFA enrollment before accessing the application.
Setup MFA
POST /api/v1/auth/mfa/setup
Requires: JWT authentication
Generates a new TOTP secret, encrypts it with AES-256-GCM, and stores it temporarily in Valkey (10min TTL). Returns the secret and provisioning URI for QR code display, along with 8 recovery codes.
Response: 200 OK
{
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"qr_code_uri": "otpauth://totp/Proxima%20Console:[email protected]?...",
"recovery_codes": [
"a1b2-c3d4",
"e5f6-g7h8",
"..."
]
}
}
Recovery codes are shown only once. Users must save them securely before confirming MFA setup.
Confirm MFA
POST /api/v1/auth/mfa/confirm
Requires: JWT authentication
Request:
{
"code": "123456"
}
Validates the TOTP code against the pending secret in Valkey. On success, persists the encrypted secret and bcrypt-hashed recovery codes to the database, enabling MFA on the account.
Verify MFA (During Login)
POST /api/v1/auth/mfa/verify
Request:
{
"mfa_token": "uuid-challenge-token",
"code": "123456"
}
Validates the MFA challenge token (single-use, 5min TTL) and the TOTP code. The code field accepts either a 6-digit TOTP code or a recovery code (xxxx-xxxx format). Recovery codes are single-use: consumed on successful verification.
Response: 200 OK (same format as login success with access_token and refresh_token)
Disable MFA
DELETE /api/v1/auth/mfa
Requires: JWT authentication
Request:
{
"password": "current-password"
}
Requires current password confirmation. Removes MFA secret and recovery codes from the account.
Password Management
Change Password
PUT /api/v1/auth/password
Requires: JWT authentication
Request:
{
"current_password": "old-password",
"new_password": "new-secure-password"
}
Validates the current password, enforces the password policy (NIST 800-63B: 12+ characters, no complexity rules, 10K common password blocklist), and checks against the 5 most recent passwords to prevent reuse. On success, updates the password and records the old hash in the password history.
Forgot Password
POST /api/v1/auth/forgot-password
Request:
{
"email": "[email protected]"
}
Response: Always 200 OK regardless of whether the email exists (prevents email enumeration).
{
"data": {
"message": "If that email is registered, a reset link has been sent."
}
}
If the user exists, generates a token (32 random bytes, SHA-256 hash stored in user_tokens, 1h expiry), invalidates any previous reset tokens for the user, and sends an email asynchronously.
Reset Password
POST /api/v1/auth/reset-password
Request:
{
"token": "base64url-encoded-token-from-email",
"new_password": "new-secure-password"
}
Validates the token (non-expired, non-used), enforces the password policy, updates the password, marks the token as used, and revokes all existing sessions.
Invitation Flow
Accept Invitation
POST /api/v1/auth/accept-invite
Request:
{
"token": "base64url-encoded-invite-token",
"password": "new-secure-password"
}
Validates the invitation token, sets the user's password, activates the account (status: invited -> active), marks the token as used, issues JWT tokens, and creates a session. Returns the same response format as login.
Session Management
List Sessions
GET /api/v1/auth/sessions
Requires: JWT authentication
Returns all active (non-revoked, non-expired) sessions for the authenticated user.
Response: 200 OK
{
"data": [
{
"id": "session-uuid",
"user_id": "user-uuid",
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"device_label": "Chrome on macOS",
"is_revoked": false,
"last_active_at": "2026-02-23T10:00:00Z",
"expires_at": "2026-03-02T10:00:00Z",
"created_at": "2026-02-23T10:00:00Z"
}
]
}
Revoke Session
DELETE /api/v1/auth/sessions/:id
Requires: JWT authentication
Revokes a specific session. Users can only revoke their own sessions (ownership verified by session.UserID == authContext.SubjectID).
Revoke All Sessions
DELETE /api/v1/auth/sessions
Requires: JWT authentication
Revokes all sessions for the authenticated user. Useful for "log out everywhere" functionality.
Stateless JWT tokens remain valid until they expire (15 minutes by default). Revoking a session prevents token refresh but does not immediately invalidate existing access tokens. The frontend handles this by automatically logging out the user when they revoke their own session or all sessions.
Token Refresh
POST /api/v1/auth/refresh
Request:
{
"refresh_token": "base64url-encoded-refresh-token"
}
Rotates the refresh token (old token invalidated, new one issued) and returns a new access token. Validates that the session is not revoked, not expired, and the user is still active.
Logout
POST /api/v1/auth/logout
Requires: JWT authentication
Request:
{
"refresh_token": "base64url-encoded-refresh-token"
}
Revokes the session associated with the provided refresh token.
Current User
GET /api/v1/auth/me
Requires: JWT authentication
Returns the authenticated user's profile and resolved client access map.
Response: 200 OK
{
"data": {
"user": {
"id": "user-uuid",
"email": "[email protected]",
"display_name": "Jane Doe",
"mfa_enabled": true,
"status": "active"
},
"client_access": {
"client-uuid-1": ["hosts:read", "metrics:read"],
"client-uuid-2": ["hosts:read", "hosts:write"]
}
}
}
Google OIDC SSO
Proxima Console supports "Sign in with Google" for Google Workspace users. When configured, team members can authenticate with their Google accounts instead of email/password.
Prerequisites
- A Google Cloud project with OAuth 2.0 credentials (Web application type)
- The authorized redirect URI set to
{BACKEND_URL}/api/v1/auth/google/callback - A Google Workspace domain to restrict access (e.g.,
proximaops.io)
Configuration
| Variable | Default | Description |
|---|---|---|
PROXIMA_GOOGLE_CLIENT_ID | — | Google OAuth 2.0 client ID (SSO disabled when empty) |
PROXIMA_GOOGLE_CLIENT_SECRET | — | Google OAuth 2.0 client secret |
PROXIMA_GOOGLE_ALLOWED_DOMAIN | proximaops.io | Restrict to this Google Workspace domain |
PROXIMA_BACKEND_URL | http://localhost:8080 | Backend base URL (used to construct the OIDC redirect URI) |
When PROXIMA_GOOGLE_CLIENT_ID is empty, Google SSO is disabled and the routes are not registered.
Authentication Flow
Frontend Backend Google
│ │ │
│ GET /auth/google │ │
├──────────────────────────>│ │
│ │── generate state (Valkey) ──> │
│ 307 redirect to Google │ │
│<──────────────────────────│ │
│ │ │
│ User authenticates ──────┼──────────────────────────────>│
│ │ │
│ │ GET /auth/google/callback │
│ │<──────────────────────────────│
│ │── validate state │
│ │── exchange code for tokens │
│ │── verify ID token (hd, email) │
│ │── find/create/link user │
│ │── generate JWT + refresh │
│ │── create one-time auth code │
│ │ │
│ 307 redirect to frontend │ │
│ ?code={one_time_code} │ │
│<──────────────────────────│ │
│ │ │
│ POST /auth/google/exchange │
│ { "code": "..." } │ │
├──────────────────────────>│ │
│ │── redeem auth code │
│ { access_token, │ │
│ refresh_token } │ │
│<──────────────────────────│ │
Endpoints
Initiate Google Login
GET /api/v1/auth/google
Redirects (307) to Google's authorization endpoint with a CSRF state parameter and hd domain hint. No authentication required.
Google Callback
GET /api/v1/auth/google/callback
Handles the OAuth 2.0 callback from Google:
- Validates the state parameter (single-use, GETDEL from Valkey) and the browser-binding state cookie
- Exchanges the authorization code for Google tokens
- Verifies the ID token: checks the
nonceclaim matches the nonce bound to this flow at auth-URL time, thehdclaim matches the allowed domain (when configured), andemail_verifiedis true - Finds or creates the user (subject to the auto-provision gate, below):
- Existing by provider ID: returns the user (already linked) — always allowed
- Existing by email: links the Google identity to the existing account — only when an allowed domain is configured
- New user: auto-provisions with
status=active,auth_provider=google— only when an allowed domain is configured
- Generates JWT access token + refresh token + session
- Creates a one-time auth code (stored in Valkey, 5min TTL)
- Redirects to
{FRONTEND_URL}/auth/google/callback?code={one_time_code}
Exchange Auth Code
POST /api/v1/auth/google/exchange
Request:
{
"code": "one-time-auth-code-from-redirect"
}
Response: 200 OK
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "base64url-encoded-random-bytes",
"session_id": "session-uuid",
"token_type": "Bearer",
"expires_in": 900
}
}
User Account Linking
When a Google user signs in for the first time:
- If a user with the same email already exists (e.g., created via invitation), the Google identity is linked to the existing account (
auth_providerandauth_provider_idare updated) — only when an allowed domain is configured - If no matching user exists, a new account is auto-provisioned with
status=activeand no password (Google-only authentication) — only when an allowed domain is configured - Subsequent logins use the provider ID for fast lookup
Auto-provisioning and email-based auto-linking are gated on PROXIMA_GOOGLE_ALLOWED_DOMAIN. When the allowed domain is unset or empty, the server will not create new accounts or link a Google identity to an existing account purely from a verified Google email — otherwise any Google account on the internet could become an active user, or an attacker controlling a victim's email at Google could take over a pre-existing local account. With the domain unconfigured, SSO succeeds only for an account that has already been explicitly linked by provider ID. Configure PROXIMA_GOOGLE_ALLOWED_DOMAIN to your Google Workspace domain to enable open provisioning safely (the verified hd claim is then enforced for every account created or linked). An empty string is treated the same as unset.
Security
- State parameter: 32 random bytes, SHA-256 hashed, stored in Valkey with GETDEL (single-use, 5min TTL), plus a browser-binding HttpOnly state cookie (login-CSRF defense)
- Nonce: a single-use random nonce is bound to the flow and sent in the auth request; the
nonceclaim in the returned ID token is validated against it (ID-token replay / injection defense) - Domain restriction:
hdclaim enforced server-side in ID token validation (the URL parameter alone is bypassable by users outside the domain) - Auto-provision gate: account creation/linking from a verified Google identity requires a configured allowed domain (see warning above)
- Email verification: only verified Google emails are accepted
- One-time auth code: JWT tokens never appear in redirect URLs, preventing exposure in browser history and referrer headers
- MFA: when a linked Console user has MFA enabled, the SSO callback issues an intermediate MFA challenge instead of a full session
Auth Configuration
GET /api/v1/auth/config
A public endpoint (no authentication required) that returns the current auth configuration. The frontend queries this on the login page to determine which authentication options to display.
Response: 200 OK
{
"data": {
"google_sso_enabled": true
}
}
| Field | Type | Description |
|---|---|---|
google_sso_enabled | boolean | Whether Google OIDC SSO is configured and available. true when PROXIMA_GOOGLE_CLIENT_ID is set. |
The frontend uses this to conditionally render the "Sign in with Google" button on the login page.
Admin User Seeding
On startup, the backend seeds an initial super-admin user if PROXIMA_ADMIN_EMAIL and PROXIMA_ADMIN_PASSWORD are set. This is idempotent — if a user with that email already exists, seeding is skipped.
The seeded user has:
- Status:
active(no invitation required) - Super admin:
true(bypasses all permission checks)
PROXIMA_ADMIN_EMAIL=[email protected]
PROXIMA_ADMIN_PASSWORD=a-strong-password-here
After the first login, you can create additional admin users through the Admin API and unset these environment variables.
RBAC & Permissions
Proxima Console uses Role-Based Access Control (RBAC) with per-client scoping. Permissions are granted through roles assigned to users via teams or direct client assignments.
Permission Format
Permissions follow the resource:action pattern:
| Resource | Actions | Description |
|---|---|---|
clients | read, write, delete | Client management |
environments | read, write, delete | Environment management |
hosts | read, write, delete | Host management |
users | read, write, delete | User and service account management |
roles | read, write, delete | Role management |
teams | read, write, delete | Team management |
credentials | read, write, delete | Credential (secret) management |
metrics | read | View metrics and dashboards |
integrations | read, write | Integration management |
tags | read, write | Entity tag management |
search | read, write | PQL search and saved searches |
processes | read | View process lists |
changes | read | View change detection |
logs | read | View logs |
alerts | read, write | Alert management (view, acknowledge, manage) |
healthcheck | read, write | Healthcheck rules and triggers |
chat | read, write | AI chat (access, send messages) |
audit | read | View audit log |
system | read, write | System settings |
webhooks | read, write, delete | Webhook source management |
compliance | read, write, delete | Compliance frameworks and controls |
alertsources | read, write, delete | Alert sources and label mappings |
runbooks | read, write, execute | Runbooks, versions, actions, executions |
assets | read, write | CMDB assets |
agents | read, update | Agent fleet status and self-updates (rollouts) |
terminal | write | Connect to host terminals |
terminal_sessions | read, write | View / force-terminate terminal sessions |
servicedesk | read, write, delete | Service desk tickets and metrics |
servicedeskconfig | read, write, delete | Service desk organization mappings |
telegram_ops | write | Ops bot: act on alerts for a linked Telegram user |
GET /api/v1/permissions always returns the authoritative, current set of resource:action pairs.
System Roles
Eight built-in roles are seeded by migration. System roles cannot be modified or deleted.
| Role | Description | Key Permissions |
|---|---|---|
| Super Admin | Full system access | All permissions including roles:write, system:write |
| Admin | Full access except role management | All except roles:write, roles:delete, teams:delete, system:read/write |
| Team Lead | Engineer access plus user visibility and audit | users:read, teams:read, roles:read, audit:read + engineer permissions |
| Engineer | Day-to-day operations | hosts:read/write, metrics:read, integrations:read/write, tags:read/write, search:read/write, chat:read |
| Executive | Read-only overview | clients:read, hosts:read, metrics:read, integrations:read, healthcheck:read |
| Viewer | Minimal read-only | hosts:read, metrics:read, healthcheck:read |
| Client | External client user (read-only infra status + Service Desk) | clients:read, hosts:read, assets:read, servicedesk:read/write |
| Client Engineer | Technical client user — read-only operational visibility plus CMDB and Service Desk | Client permissions + metrics:read, logs:read, processes:read, changes:read, alerts:read, healthcheck:read, compliance:read |
Custom roles can be created via POST /api/v1/roles with any combination of permissions.
Access Resolution
Users gain permissions through two paths, which are combined (union):
Team Path: User → Team Membership (role) → Team's Clients → Permissions per client
Direct Path: User → Direct Client Assignment (role) → Permissions for that client
When a user's access is resolved, the system queries both paths and merges the permission sets per client. If a user has hosts:read via a team and hosts:write via a direct assignment for the same client, they get both permissions.
Resolution is cached in Valkey (15-minute TTL) with a database fallback. The cache is invalidated immediately when access-affecting changes are made (role updates, team membership changes, client assignments).
Super Admins
Users with is_super_admin=true bypass all permission checks. They:
- See all clients and resources without explicit assignments
- Can perform any action on any resource
- Are not subject to client-scoped access rules
The initial super admin is created via the PROXIMA_ADMIN_EMAIL / PROXIMA_ADMIN_PASSWORD environment variables on first startup.
Service Account Permissions
Service accounts use API keys (X-API-Key header) instead of JWT tokens. Each service account has:
- A role that determines its permissions
- A client scope (specific client IDs or
["*"]for wildcard access)
Wildcard service accounts (client_scope: ["*"]) can access all clients using their role's permissions.
Admin APIs
Admin endpoints manage users, teams, roles, service accounts, and audit logs. All require JWT authentication and specific RBAC permissions. The frontend provides a complete admin UI for all these operations — see the Frontend Overview for details on the admin pages.
See the API Endpoints Reference for full endpoint documentation.
Permission Requirements
| Resource | Read | Write | Delete |
|---|---|---|---|
| Users | users:read | users:write | users:delete |
| Teams | teams:read | teams:write | teams:delete |
| Roles | roles:read | roles:write | roles:delete |
| Service Accounts | users:read | users:write | users:delete |
| Audit Log | audit:read | — | — |
Additional specialized endpoints:
| Operation | Endpoint | Permission |
|---|---|---|
| Get role usage | GET /api/v1/roles/:id/usage | roles:read |
| Access audit by user | GET /api/v1/admin/access-audit/by-user/:userId | roles:read OR users:read |
| Access audit by client | GET /api/v1/admin/access-audit/by-client/:clientId | roles:read OR clients:read |
Super admins (is_super_admin=true) bypass all permission checks.
Role Usage
The role usage endpoint (GET /api/v1/roles/:id/usage) returns the users and teams that are currently assigned a given role. This powers the "Used By" section on the role detail page, showing at a glance which users have the role via direct client assignments and which teams use the role for their members. Requires roles:read permission.
Access Audit
The Access Audit page (/admin/access-audit) provides a comprehensive view of who has access to what across the system. It supports two views:
- User-centric view (
GET /api/v1/admin/access-audit/by-user/:userId): Select a user and see all their client access, including which role grants each permission set and whether the access comes from a direct assignment or team membership. - Client-centric view (
GET /api/v1/admin/access-audit/by-client/:clientId): Select a client and see all users who have access to it, with the same role and source breakdown.
Each entry in the audit results shows:
- Role: The role granting access (e.g., "Engineer", "Admin")
- Source: Whether the access is a direct client assignment or inherited through a team (with the team name)
- Permissions: Expandable details showing the full set of permissions granted by the role
This is useful for security reviews, onboarding/offboarding verification, and answering questions like "who can access Client X?" or "what can User Y access?".
Cache Invalidation
When access-affecting data changes (role assignments, team membership, client assignments), the handler invalidates the affected user's cached access resolution in Valkey. This ensures permission changes take effect immediately without waiting for cache TTL expiry.
Audit Logging
All admin operations are recorded in the audit log with:
- Actor: user ID and email of who performed the action
- Action: what was done (e.g.,
user.created,role.updated,team.member.added) - Resource: type and ID of the affected resource
- Details: JSON payload with relevant details (e.g., role name, permissions changed)
- IP address and timestamp
Query the audit log via GET /api/v1/audit-log with filters for actor, action, resource type, resource ID, and date range (RFC3339).
Security Details
Password Policy
NIST 800-63B compliant:
- Minimum 12 characters
- No complexity rules (uppercase, symbols, etc.)
- 10,000 common password blocklist via
go:embed - 5-password reuse prevention (bcrypt comparison against
password_historytable)
TOTP Configuration
- Algorithm: SHA-1 (standard TOTP)
- Period: 30 seconds
- Digits: 6
- Skew: 1 (accepts codes from previous and next period)
- Secret encryption: AES-256-GCM with 12-byte nonce prepended to ciphertext
Recovery Codes
- 8 codes generated per MFA setup
- Format:
xxxx-xxxx(8 hex characters with hyphen) - Storage: bcrypt-hashed (normalized: strip hyphens, lowercase)
- Single-use: consumed on successful verification
Progressive Lockout
Failed login attempts trigger progressive lockout:
| Failures | Lockout Duration |
|---|---|
| 5 | 1 minute |
| 10 | 5 minutes |
| 15 | 15 minutes |
| 20+ | 1 hour |
Lockout state is stored in Valkey with in-memory fallback for graceful degradation.
Token Security
- Access tokens: HS256 JWT, 15-minute TTL (configurable via
PROXIMA_JWT_ACCESS_TTL) - Refresh tokens: 32 cryptographically random bytes, base64url-encoded. SHA-256 hash stored in database. 7-day TTL (configurable via
PROXIMA_JWT_REFRESH_TTL). - Password reset / invite tokens: 32 random bytes, SHA-256 hash in
user_tokenstable, 1h expiry (reset) / 24h expiry (invite) - MFA challenge tokens: UUID stored in Valkey with 5-minute TTL, single-use (deleted after validation)
Configuration
| Variable | Default | Description |
|---|---|---|
PROXIMA_JWT_SECRET | — | JWT signing secret (required for auth) |
PROXIMA_JWT_ACCESS_TTL | 900 | Access token TTL in seconds |
PROXIMA_JWT_REFRESH_TTL | 604800 | Refresh token TTL in seconds |
PROXIMA_VALKEY_URL | — | Valkey (Redis-compatible) URL for lockout, MFA tokens, and access cache |
PROXIMA_MFA_ENCRYPTION_KEY | — | Hex-encoded 32-byte AES-256 key for TOTP secret encryption |
PROXIMA_EMAIL_PROVIDER | smtp | Email provider: smtp or resend |
PROXIMA_EMAIL_FROM | [email protected] | Sender email address |
PROXIMA_EMAIL_FROM_NAME | Proxima Console | Sender display name |
PROXIMA_RESEND_API_KEY | — | Resend API key (when provider is resend) |
PROXIMA_SMTP_HOST | — | SMTP server hostname |
PROXIMA_SMTP_PORT | 587 | SMTP server port |
PROXIMA_SMTP_USERNAME | — | SMTP authentication username |
PROXIMA_SMTP_PASSWORD | — | SMTP authentication password |
PROXIMA_FRONTEND_URL | https://app-console.prxm.uz | Frontend URL for email links (reset, invite) |