feat: SSO from GTI Gerenciador and the Gerenciador visual identity

Collaborators logged into GTI Gerenciador open GTI Fiscal without a
second password. The panel signs a 60 s JWT (Ed25519; the Hub holds only
the public key); the Hub checks signature, iss/aud/exp/nbf, maximum
lifetime, nonce against a one-time state cookie set when the flow starts
at GET /login (login-CSRF protection) and a single-use jti
(sso_used_assertions). Collaborators are provisioned in admin_users with
auth_source=GTI_GERENCIADOR and no password; a CHECK constraint makes a
passwordless local account or a federated account with a password
impossible (migration 0021). Sessions last FEDERATED_SESSION_SECONDS
(1 h) and are renewed silently through the panel.

The local admin stays as break-glass at /login/local, audited. Logout ends
only the Hub session. Every SSO success/rejection (stable reason code),
logout and local login is audited with the real client IP, never the
token. verify_password now fails closed on a missing hash instead of
raising.

Visual identity: gti-theme.css is a verbatim copy of the panel's
design system (tokens, sidebar, light/dark theme) plus its tema.js,
sidebar.js and logo; control-panel.css keeps only Hub components, all
colours from the panel tokens. Page structure and features unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-23 12:21:26 -03:00
co-authored by Claude Opus 5.5
parent 91a2dda969
commit 64d3fc56e2
21 changed files with 1290 additions and 122 deletions
+38
View File
@@ -155,6 +155,44 @@ generate absolute links, but keep it accurate for anyone reading the config).
`gti-fiscal-hub-port.service` (instructions in the unit file) so only the
NPM reaches the port, re-applied on every Docker start.
## Human access: SSO from GTI Gerenciador
Collaborators never type a second password. GTI Gerenciador (the panel) is the
identity authority; the Hub keeps its own sessions.
1. "Fiscal" in the panel sidebar -> panel `/sso/fiscal` -> Hub `GET /login`,
which sets a one-time state cookie (`gti_sso_state`, HttpOnly, Secure,
SameSite=None, 5 min) and redirects to `SSO_START_URL?state=...&next=...`.
2. The panel requires its own login (normal login page if needed), checks the
employee is active and auto-POSTs a JWT to `/auth/sso/callback`:
Ed25519 (`EdDSA` only), `iss=gti-gerenciador`, `aud=gti-fiscal`,
`sub=funcionario:<ID>`, 60 s, unique `jti`, `nonce`=state. Never in a URL.
3. The Hub verifies signature/iss/aud/exp/nbf/lifetime, the nonce against the
state cookie (login-CSRF protection), records the `jti`
(`sso_used_assertions`, replay = rejected), provisions or refreshes the
collaborator in `admin_users` (`auth_source=GTI_GERENCIADOR`, no password,
enforced by `ck_admin_users_auth_source`) and opens a session of
`FEDERATED_SESSION_SECONDS` (default 1 h; afterwards the same flow signs the
user in again silently, which also picks up a deactivation in the panel).
Keys: a dedicated Ed25519 pair, used for nothing else (never reuse
SESSION_SECRET, admin, Agent or DB secrets). The **private** key
(raw 32-byte seed, base64url) goes only to the panel's `.env`
(`SSO_FISCAL_CHAVE_PRIVADA`); the **public** key only to this Hub's `.env`
(`SSO_GERENCIADOR_CHAVE_PUBLICA`). Rotating = new pair in both, restart both.
Logout ("Sair do Fiscal") ends only the Hub session; coming back through the
panel signs in again without a password. There is no distributed single logout.
Audit (`audit_events`): `SSO_LOGIN_SUCCESS`, `SSO_LOGIN_REJECTED` (with a stable
reason: `EXPIRED`, `INVALID_SIGNATURE`, `WRONG_AUDIENCE`, `WRONG_ISSUER`,
`REPLAY`, `STATE_MISMATCH`, `DISABLED_IN_FISCAL`, ...), `LOGOUT`,
`LOGIN_LOCAL_SUCCESS/FAILURE` — with the real client IP, never the token.
**Break-glass:** the local admin (`create_admin.py`) still exists for
emergencies at `/login/local` (not linked from the UI; every use is audited).
Disable a collaborator only in the Hub: `admin_users.is_active = false`.
## Registering an Agent (per client installation)
Run once per Agent installation, from anywhere with access to Postgres —