Skip to main content

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:

ModeHeaderUse Case
JWT BearerAuthorization: Bearer <jwt>User login sessions (frontend, API clients)
API KeyX-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:

  1. Check progressive lockout (5 failures: 1min, 10: 5min, 15: 15min, 20+: 1h)
  2. Look up user by email (same 401 for wrong email or wrong password to prevent enumeration)
  3. Verify account status is active
  4. Verify password (bcrypt)
  5. If MFA is enabled, return 202 Accepted with an MFA challenge token
  6. Issue JWT access token (15min TTL) + refresh token (7-day TTL)
  7. Create session record with device label and IP address
  8. 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:

VariableDefaultDescription
PROXIMA_API_RATE_LIMIT1000General API rate limit (requests/min/IP)
PROXIMA_AUTH_RATE_LIMIT5Public 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",
"..."
]
}
}
warning

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.

note

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

VariableDefaultDescription
PROXIMA_GOOGLE_CLIENT_IDGoogle OAuth 2.0 client ID (SSO disabled when empty)
PROXIMA_GOOGLE_CLIENT_SECRETGoogle OAuth 2.0 client secret
PROXIMA_GOOGLE_ALLOWED_DOMAINproximaops.ioRestrict to this Google Workspace domain
PROXIMA_BACKEND_URLhttp://localhost:8080Backend 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:

  1. Validates the state parameter (single-use, GETDEL from Valkey) and the browser-binding state cookie
  2. Exchanges the authorization code for Google tokens
  3. Verifies the ID token: checks the nonce claim matches the nonce bound to this flow at auth-URL time, the hd claim matches the allowed domain (when configured), and email_verified is true
  4. 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
  5. Generates JWT access token + refresh token + session
  6. Creates a one-time auth code (stored in Valkey, 5min TTL)
  7. 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_provider and auth_provider_id are updated) — only when an allowed domain is configured
  • If no matching user exists, a new account is auto-provisioned with status=active and no password (Google-only authentication) — only when an allowed domain is configured
  • Subsequent logins use the provider ID for fast lookup
Auto-provision requires an allowed domain

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 nonce claim in the returned ID token is validated against it (ID-token replay / injection defense)
  • Domain restriction: hd claim 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
}
}
FieldTypeDescription
google_sso_enabledbooleanWhether 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
tip

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:

ResourceActionsDescription
clientsread, write, deleteClient management
environmentsread, write, deleteEnvironment management
hostsread, write, deleteHost management
usersread, write, deleteUser and service account management
rolesread, write, deleteRole management
teamsread, write, deleteTeam management
credentialsread, write, deleteCredential (secret) management
metricsreadView metrics and dashboards
integrationsread, writeIntegration management
tagsread, writeEntity tag management
searchread, writePQL search and saved searches
processesreadView process lists
changesreadView change detection
logsreadView logs
alertsread, writeAlert management (view, acknowledge, manage)
healthcheckread, writeHealthcheck rules and triggers
chatread, writeAI chat (access, send messages)
auditreadView audit log
systemread, writeSystem settings
webhooksread, write, deleteWebhook source management
complianceread, write, deleteCompliance frameworks and controls
alertsourcesread, write, deleteAlert sources and label mappings
runbooksread, write, executeRunbooks, versions, actions, executions
assetsread, writeCMDB assets
agentsread, updateAgent fleet status and self-updates (rollouts)
terminalwriteConnect to host terminals
terminal_sessionsread, writeView / force-terminate terminal sessions
servicedeskread, write, deleteService desk tickets and metrics
servicedeskconfigread, write, deleteService desk organization mappings
telegram_opswriteOps 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.

RoleDescriptionKey Permissions
Super AdminFull system accessAll permissions including roles:write, system:write
AdminFull access except role managementAll except roles:write, roles:delete, teams:delete, system:read/write
Team LeadEngineer access plus user visibility and auditusers:read, teams:read, roles:read, audit:read + engineer permissions
EngineerDay-to-day operationshosts:read/write, metrics:read, integrations:read/write, tags:read/write, search:read/write, chat:read
ExecutiveRead-only overviewclients:read, hosts:read, metrics:read, integrations:read, healthcheck:read
ViewerMinimal read-onlyhosts:read, metrics:read, healthcheck:read
ClientExternal client user (read-only infra status + Service Desk)clients:read, hosts:read, assets:read, servicedesk:read/write
Client EngineerTechnical client user — read-only operational visibility plus CMDB and Service DeskClient 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

ResourceReadWriteDelete
Usersusers:readusers:writeusers:delete
Teamsteams:readteams:writeteams:delete
Rolesroles:readroles:writeroles:delete
Service Accountsusers:readusers:writeusers:delete
Audit Logaudit:read

Additional specialized endpoints:

OperationEndpointPermission
Get role usageGET /api/v1/roles/:id/usageroles:read
Access audit by userGET /api/v1/admin/access-audit/by-user/:userIdroles:read OR users:read
Access audit by clientGET /api/v1/admin/access-audit/by-client/:clientIdroles: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_history table)

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:

FailuresLockout Duration
51 minute
105 minutes
1515 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_tokens table, 1h expiry (reset) / 24h expiry (invite)
  • MFA challenge tokens: UUID stored in Valkey with 5-minute TTL, single-use (deleted after validation)

Configuration

VariableDefaultDescription
PROXIMA_JWT_SECRETJWT signing secret (required for auth)
PROXIMA_JWT_ACCESS_TTL900Access token TTL in seconds
PROXIMA_JWT_REFRESH_TTL604800Refresh token TTL in seconds
PROXIMA_VALKEY_URLValkey (Redis-compatible) URL for lockout, MFA tokens, and access cache
PROXIMA_MFA_ENCRYPTION_KEYHex-encoded 32-byte AES-256 key for TOTP secret encryption
PROXIMA_EMAIL_PROVIDERsmtpEmail provider: smtp or resend
PROXIMA_EMAIL_FROM[email protected]Sender email address
PROXIMA_EMAIL_FROM_NAMEProxima ConsoleSender display name
PROXIMA_RESEND_API_KEYResend API key (when provider is resend)
PROXIMA_SMTP_HOSTSMTP server hostname
PROXIMA_SMTP_PORT587SMTP server port
PROXIMA_SMTP_USERNAMESMTP authentication username
PROXIMA_SMTP_PASSWORDSMTP authentication password
PROXIMA_FRONTEND_URLhttps://app-console.prxm.uzFrontend URL for email links (reset, invite)