---
module: technical.auth
title: hiveops-auth — Customer authentication + TOTP MFA + JWT issuer
tab: Overview
order: 10
audience: dev
---

> **DRAFT · internal.** Written 2026-07-01 from hiveops-auth source. Verify against code before relying on specifics.

## Responsibility

Owns **customer** authentication only — internal/admin auth lives in `hiveops-mgmt`. Handles login, TOTP/backup-code MFA, JWT issuance + refresh, self-service registration, password reset, and account lockout. It also issues the JWTs that other microservices validate via the shared `JWT_SECRET` (see `hiveops-security-common`). Serves both a JSON API (`/auth/api/**`) and server-rendered Thymeleaf login UI (`/auth/**`). Cross-link [platform.service-ownership].

- Spring Boot 3.4.1, Java 21, port **8082** (`${PORT:8082}`).
- JWT generation resolves institution keys by calling `hiveops-mgmt` (`mgmt-service.url`, default `http://hiveiq-mgmt:8080`).
- MSP domain (`auth.msp-domain`, default `bcos.cloud`) is granted `MSP_ADMIN` role in the JWT.

## Data

**Database:** `hiveiq_auth` (PostgreSQL in prod, H2 in-memory in dev). Note: the prod JDBC URL is built from `DB_HOST`/`DB_PORT`/`DB_NAME` (env `DB_NAME` default in code is `hiveops_auth`, but the deployed DB is `hiveiq_auth`) — not a full `DB_URL`. Cross-link [platform.data-stores].

Key tables/entities (Flyway `db/migration`, prod only; dev uses `ddl-auto: create-drop`):

| Table | Purpose |
|-------|---------|
| `users` | Customer accounts, password hash, role, MFA secret (AES-encrypted), lockout/attempt counters |
| `mfa_backup_codes` | 10 BCrypt-hashed single-use backup codes per user |
| `auth_sessions` | Short-lived (5 min) intermediate MFA sessions between login step 1 and 2 |
| `refresh_tokens` | Issued refresh tokens |
| `auth_audit_logs` | Auth event audit trail (queried by AuditAdminController) |
| `customer_domains` | Email-domain → institution key/name mapping |
| `password_reset_tokens` | 1-hour password reset tokens |
| `SPRING_SESSION` | Spring Session JDBC store (server-rendered UI sessions, auto-created) |

## Kafka

No Kafka. No producers, listeners, `KafkaTemplate`, or `StreamBridge` in the service. Cross-link [platform.kafka].

## API surface

Base paths: `/auth/api` (JSON), `/auth` (Thymeleaf UI). Most important JSON endpoints:

| Method Path | Purpose |
|-------------|---------|
| POST `/auth/api/login` | Step 1 — credentials (field is **`email`**, not username); returns token or `mfaRequired` + sessionToken |
| POST `/auth/api/verify-mfa` | Step 2a — verify TOTP code, return access + refresh token |
| POST `/auth/api/verify-backup-code` | Step 2b — verify backup code |
| POST `/auth/api/refresh` | Refresh access token |
| POST `/auth/api/logout` | Invalidate session/token |
| POST `/auth/api/change-password` | Change password (authenticated) |
| POST `/auth/api/register` | Self-service registration |
| GET `/auth/api/mfa/status` | MFA enabled? backup codes remaining? |
| POST `/auth/api/mfa/setup` | Begin MFA setup → QR + secret |
| POST `/auth/api/mfa/confirm` | Confirm TOTP to activate MFA |
| POST `/auth/api/mfa/disable` | Disable MFA |
| POST `/auth/api/mfa/regenerate-backup-codes` | Issue new backup codes |
| GET/PUT `/auth/api/users/me` | Current user profile; `/preferences` sub-paths |
| GET `/auth/api/docs-validate` | Docs-site JWT validation (DocsAuthController) |

Admin / service-to-service (not exhaustive):

| Method Path | Guard |
|-------------|-------|
| GET `/auth/api/msp/users`, PUT/POST `/auth/api/msp/users/{id}/...` | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |
| GET `/auth/api/admin/audit/logs`, `/summary` | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |
| `/auth/api/admin/rate-limit/**`, `/auth/api/admin/users/**` | `hasRole('ADMIN')` |
| `/auth/api/internal/users/**`, `/domains/**` | `hasRole('SERVICE')` (shared `AUTH_SERVICE_TOKEN`) |

## Gotchas

- **Login field is `email`, not `username`** — wrong field returns 403.
- **Two role vocabularies:** MSP/audit controllers use `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`; rate-limit and `/admin/users` controllers use `hasRole('ADMIN')`; internal endpoints use `hasRole('SERVICE')`. They are not interchangeable.
- **Fail-fast secrets:** `JWT_SECRET` and `MFA_SECRET_ENCRYPTION_KEY` have no defaults — the app will not start without them. `JWT_SECRET` must match `hiveops-mgmt` exactly (shared token validation).
- **Refresh expiration is 1 day**, despite older docs mentioning 7 days — `jwt.refresh-expiration` and `short-refresh-expiration` are both `86400000` (24h); "remember me" now only saves the email, not a long session.
- **MFA:** SHA1, 6 digits, 30s period; secret AES-encrypted at rest; 10 BCrypt-hashed single-use backup codes.
- **Lockout:** 5 failed attempts → 15-minute lockout (`auth.max-login-attempts` / `auth.lockout-duration`). Unlock via SQL: `UPDATE users SET failed_login_attempts=0, locked_until=NULL WHERE email=...`.
- **Rate limiting** is in-process (bucket4j-style config under `rate-limit`): IP bucket 20/min, user bucket 5/15min.
- **CORS_ALLOWED_ORIGINS is required** — service refuses to start without it; new frontend origins must be added here AND in `hiveops-mgmt`.
- **Password expiry / inactivity:** forced password change after 90 days; account auto-disabled after 90 days inactivity (`auth.password-expiry-days` / `inactivity-disable-days`).
- **Dev profile** uses H2 in-memory with Flyway disabled and seeds `db/h2/data.sql`; Swagger + H2 console only enabled via `SWAGGER_ENABLED` / dev profile.
