---
module: platform.auth
title: Auth & JWT Flow
tab: Overview
order: 40
audience: dev
---

> **DRAFT · internal** (2026-07-01). Verify claim names / secret handling against
> `hiveops-security-common` before relying on specifics.

## Who issues what
- **hiveops-auth** authenticates customers (TOTP MFA) and issues the **JWT**. Login is
  `POST /auth/api/login` with body field **`email`** (not `username`), response field **`token`**.
- **hiveops-mgmt** handles internal/admin; shares the **same `JWT_SECRET`** as auth.
- The `JWT_SECRET` must be **identical** across services that validate tokens.

## How services validate (shared lib)
`hiveops-security-common` `JwtTokenValidator`: key = `Keys.hmacShaKeyFor(jwtSecret.getBytes())`
(the secret is used as **raw bytes — do NOT base64-decode it**). Claims read:
`sub` (numeric user id), **`email`**, **`role`** (single string), `institutionKey(s)`, `type`.
Each service's `JwtAuthenticationFilter` sets a single authority `ROLE_<role>`.

## Roles
Admin roles are **`MSP_ADMIN`, `BCOS_ADMIN`** (and `QDS_ADMIN`). Most `@PreAuthorize` uses
`hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`. Audience gating (e.g. the guide's internal tabs) keys off
these admin roles.

## The HiveIQ Browser injects auth
The desktop **HiveIQ Browser** (Electron) injects `Authorization: Bearer <jwt>` **and**
`X-HiveIQ-Browser: true` on every module request (`onBeforeSendHeaders`). So the SPAs often carry
**no token interceptor themselves** — the browser adds it. `X-HiveIQ-Browser: true` is a reliable
server-side signal for "this is the desktop app" (used for the Browser-required redirect).

## Minting a token for tests (no real account)
`pyjwt` with the env `JWT_SECRET`, HS256, claims `{sub:'1', email, role, iat, exp}`. Secret must be
≥32 bytes for HS256. Route calls through nginx (`api.bcos.*/<svc>/...`).
