---
module: profile.security
title: Security
tab: Internal
order: 20
audience: internal
---

> **Internal · ops/support.** Written 2026-07-01 from `hiveops-profile` frontend + `hiveops-auth` source. Verify against code before relying on specifics.

## What this screen actually does

Two self-service controls, both owned by **`hiveops-auth`** (port 8082, NGINX `/api/auth/`):

1. **Change Password** → `POST /auth/api/change-password`
2. **Session Timeout** → `PUT /auth/api/users/me/preferences` (`sessionTimeoutMinutes`)

There is **no admin-only action on this screen** — it operates on the caller's *own* account, keyed off the JWT (`@AuthenticationPrincipal`). Any authenticated user (`anyRequest().authenticated()` in `SecurityConfig`) can use it; no `MSP_ADMIN`/`BCOS_ADMIN` role is required. Admin-driven password/lockout operations are separate paths (see below).

## Roles required

| Action | Guard (confirmed in `SecurityConfig`) |
|--------|----------------------------------------|
| Change own password | authenticated (any role) |
| Save own session timeout | authenticated (any role) |
| Admin reset another user | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` via `/auth/api/msp/**`, or `hasRole('ADMIN')` via `/auth/api/admin/users/**` — **not** this screen |

## First things to check when it misbehaves

- **"Password change fails but looks valid"** → the frontend and the customer Overview say *min 8 characters*, but the backend `PasswordValidator` enforces **min 12 chars + uppercase + lowercase + digit + special char**. An 8–11 char (or non-complex) password passes the client check, then the API returns `400 {"error":"Password must be at least 12 characters"}` (or the specific rule). This is the #1 support surprise here — tell the user the real rule. (See uncertainties — this is a real doc/UX mismatch.)
- **"Passwords do not match"** → returned by the controller *before* hitting the validator when `newPassword != confirmPassword`. Frontend also blocks this locally.
- **Timeout won't save** → `sessionTimeoutMinutes` is bean-validated `@Min(5) @Max(480)`. A value outside 5–480 returns `400`. The **Save** button also stays disabled until the value actually changes (client `timeoutUnsaved` flag).
- **Preferences look reset to defaults** → `GET /auth/api/users/me/preferences` returns a fresh default `UserPreferences` (sessionTimeoutMinutes=60) when the user row has no stored prefs. If a user's `users.preferences` jsonb is null/blank they will see defaults, not an error.
- **401 on the whole screen** → expired/invalid JWT. This service is stateless (`SessionCreationPolicy.STATELESS`); the SPA must send `Authorization: Bearer <token>`.

## Common failure modes + fixes

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Change-password 400, message about length/complexity | 12-char + complexity rule | Give the user the real rule; not a bug in the API |
| Change-password 400 "Passwords do not match" | mismatch | Re-enter confirm field |
| Account locked, can't log in to reach this screen | 5 failed logins → 15-min lockout | `UPDATE users SET failed_login_attempts=0, locked_until=NULL WHERE email='...';` (DB `hiveiq_auth`) |
| User must change password on next login | `must_change_password=true` (admin reset / 90-day expiry) | Changing password here clears the flag (`setMustChangePassword(false)`) |
| Timeout saved but user not logged out at that interval | Idle-timeout enforcement is client-side, not enforced by auth's JWT (access token TTL is 24h) | Confirm the calling SPA honors the value (unverified per-app) |

## Audit / traceability

Every successful change writes an `auth_audit_logs` row with action `PASSWORD_CHANGED` (via `auditService.logPasswordChanged`). Use `AuditAdminController` (`/auth/api/admin/audit/**`, `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`) or query the table directly to confirm a change happened and when (`password_changed_at` on `users`).

## Note (security characteristic)

The authenticated `change-password` path does **not** require the user's *current* password — it trusts the bearer JWT and overwrites the hash. A stolen/leaked live token can change the password. Not a bug per se, but relevant for support/security triage.
