nis2-agile/docs/GAP_TRPG_NIS2_ALIGNMENT.md
DevEnv nis2-agile e4f9e9179e [FEAT] Allineamento NIS2 ↔ TRPG (Fasi 1-5): SSO + Sessions + Reset + Impersonate + Branding
Implementazione completa del progetto allineamento alla suite Evix (TRPG/lg231),
basato sul doc canonico docs/GAP_TRPG_NIS2_ALIGNMENT.md (5 fasi, 18 gap).

Version 1.0.0 → 1.5.0

Fase 1 — SSO Federation (v1.1.0)
- Migration 015_sso_columns: users.sso_identity_id + password_version
- application/services/SsoHelper.php (client SSO dual-mode, cURL nativo, zero deps)
- AuthController::login() + changePassword() conditional SSO (SSO_MODE=local default)

Fase 2 — Multi-device Sessions (v1.2.0)
- Migration 016_active_sessions: tabella + refresh_tokens.session_jti
- BaseController::requireAuth() verifica jti + last_activity throttle + parseDeviceLabel
- login() genera jti, logout/changePassword revoca selettiva
- GET/DELETE /auth/sessions[/{id}]
- UI settings.html tab Sicurezza con lista device + revoca

Fase 3 — Password Reset + Tenant Switcher (v1.3.0)
- Migration 017_password_reset_tokens (TTL 30min, single-use)
- POST /auth/forgot-password (risposta opaca) + reset-password
- Pagine forgot-password.html + reset-password.html (con strength bar)
- EmailService::sendPasswordReset
- POST /auth/switchContext con rotazione JWT + organization_id claim
- Dropdown tenant in sidebar esposto a tutti gli utenti con ≥2 org

Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)
- POST /auth/impersonate (super_admin o consulente stesso firm, TTL 1h, audit)
- Migration 018_user_preferences: users.theme/timezone/notif_email/notif_inapp
- GET/PUT /auth/preferences
- Sidebar footer mostra versione + changelog modal su click

Fase 5 — Branding white-label + Auth-gate (v1.5.0)
- Migration 019_firm_branding (logo/colori/brand_name per consulting firm)
- BrandingController GET /branding/current (auth opzionale) + PUT
- common.js auto-applica CSS variables al boot
- public/js/auth-gate.js (gate password client-side per docs riservati, da TRPG)

Skip motivati:
- G15 demo login: simulator esistenti coprono
- G18 refactor controllers: rinviato (~5gg, valore tecnico solo)

Cron sync SSO: AgileHub Ticket #220 aperto a team AGILEHUB per estendere
sso-password-sync.sh al DB nis2_agile_db. Prerequisito per switch SSO_MODE=dual.

Backup files: tutti i file modificati hanno .bak.pre-{fase}-{ts} sia in DEV
sia in /var/www/nis2-agile/.backups/ su Hetzner (rollback ready).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:18:35 +02:00

468 lines
23 KiB
Markdown

# Progetto di Allineamento NIS2 → TRPG (Suite Evix)
> **Scopo**: portare NIS2 Agile agli stessi standard di Auth/Tenant/Utenti/UX che TRPG ha consolidato nei 54 minor release dal 2026-04 ad oggi.
> **Stato**: PIANO (no codice applicato). Da revisionare con l'utente prima di passare alla Fase 1.
> **Riferimento snapshot**: TRPG v1.54.1 (build 20260527) — NIS2 v1.0.0 (build 20260415).
> **Data piano**: 2026-05-29.
> **Owner proposto**: NIS2 dev + supervisione cross-suite via agile-services.
---
## 0. Premesse e principi guida
1. **Standard suite**: l'autorità è `agile-services` (`172.18.0.1:4214` Tenant MS + `nexus_tenant_db.sso_identities`). TRPG è un peer che ha già adottato lo standard. NIS2 deve adottarlo, non copiare TRPG.
2. **PHP vanilla resta**: NIS2 mantiene la sua architettura (no porting a Node.js). I componenti PHP di TRPG (`SsoHelper.php`, `HubJwtVerifier.php`, schema `active_sessions`) sono riusabili **1:1** previo adattamento namespace/config.
3. **Backward compatibility**: ogni cambio è **additivo**. Utenti esistenti continuano a loggare con flusso locale fino a che il backfill SSO non li collega.
4. **Dual-mode SSO obbligatorio**: se Tenant MS down → fallback locale automatico (no down NIS2).
5. **No big bang**: 5 fasi rilasciabili indipendentemente, ognuna con rollback semplice (drop colonne / disable flag).
6. **Audit trail**: ogni nuova feature critica (impersonate, sessions revoke, password reset) logga in `audit_logs` con `severity` appropriata.
---
## 1. Quadro sinottico delle gap
| ID | Area | Priorità | Effort | Rischio | Fase |
|----|------|----------|--------|---------|------|
| G01 | SSO sync DB (`sso_identity_id`, `password_version`) | P0 | M | Basso | 1 |
| G02 | `SsoHelper` + login federato (dual-mode) | P0 | M | Medio | 1 |
| G03 | `change-password` propagato a Tenant MS | P0 | S | Basso | 1 |
| G04 | Cron sync `password_hash` da `sso_identities` | P0 | S | Basso | 1 |
| G05 | Tabella `active_sessions` + JWT con `jti` | P0 | M | Medio | 2 |
| G06 | Endpoint sessions: `GET /list`, `DELETE /revoke` | P0 | M | Basso | 2 |
| G07 | UI Settings → tab "Sessioni attive" con device list | P1 | S | Basso | 2 |
| G08 | `forgot-password` + `reset-password` + pagina HTML | P0 | M | Basso | 3 |
| G09 | `switchContext` endpoint (rotazione JWT) | P1 | M | Medio | 3 |
| G10 | Dropdown tenant in sidebar/navbar | P1 | M | Basso | 3 |
| G11 | `impersonate` per super_admin / consulente | P1 | S | Medio | 4 |
| G12 | `updatePreferences` separato da `updateProfile` | P2 | S | Basso | 4 |
| G13 | Versioning live: bump `version.json` + footer UI | P1 | S | Basso | 4 |
| G14 | Cron agent auto-bump PATCH dopo fix | P2 | S | Basso | 4 |
| G15 | Demo login dedicato (UX) | P2 | M | Basso | 5 |
| G16 | `BrandingController` white-label per consulente | P2 | M | Basso | 5 |
| G17 | Auth-gate per documenti riservati (presentation/roadmap) | P3 | XS | Nullo | 5 |
| G18 | Split monolite `AuthController` / `OrganizationController` in controller dedicati (ApiKey, ConsultingFirm, …) | P2 | L | Medio | 5 |
Effort: XS<1g, S=1-2g, M=3-5g, L>5g.
---
## 2. Fase 1 — SSO Federation (P0)
**Obiettivo**: NIS2 partecipa al Single Sign-On suite. Utenti riusano password unica tra TRPG/lg231/NIS2.
### G01 — Schema DB
**Migration `docs/sql/015_sso_columns.sql`** (creata 2026-05-29, non ancora applicata):
```sql
ALTER TABLE users
ADD COLUMN sso_identity_id INT NULL COMMENT 'FK verso nexus_tenant_db.sso_identities.id',
ADD COLUMN password_version INT NOT NULL DEFAULT 1 COMMENT 'contatore versione password SSO',
ADD INDEX idx_sso_identity (sso_identity_id);
```
**Acceptance**: colonne presenti, utenti esistenti hanno `sso_identity_id=NULL` e `password_version=1`. Nessun controller esistente si rompe.
**Rollback**: `ALTER TABLE users DROP COLUMN sso_identity_id, DROP COLUMN password_version;`
### G02 — `SsoHelper` + login federato
**File nuovo**: `application/services/SsoHelper.php` — clone adattato dal TRPG `shared/SsoHelper.php` (~150 righe, cURL nativo, zero deps).
**Env nuovi** in `.env`:
```
SSO_ENDPOINT=http://172.18.0.1:4214
SSO_TIMEOUT_MS=3000
SSO_MODE=dual # local | dual | sso_only
SSO_INTERNAL_KEY=<da vault-steward>
```
**Modifica** `AuthController::login()`:
1. Se `SSO_MODE != local`: prova `SsoHelper::login(email, password, 'nis2')`
2. Se SSO 200 OK → estrai `sso_identity_id` + `password_version` → upsert utente locale (link o crea) → sync `password_hash`
3. Se SSO 401 → ritorna 401 (no fallback se utente esiste in SSO)
4. Se SSO unreachable (timeout/connessione) → fallback locale (dual-mode)
**Acceptance**:
- Login con credenziali SSO valide → JWT emesso anche per utenti senza row locale
- Login locale di utenti pre-SSO continua a funzionare
- Test con Tenant MS spento → login locale OK (dual)
- `SSO_MODE=sso_only` → blocca login locali
### G03 — Change password propagato
**Modifica** `AuthController::changePassword()`:
- Se `currentUser['sso_identity_id'] != NULL`: chiamata `POST {SSO_ENDPOINT}/auth/sso/change-password` con JWT utente
- Se SSO ok → cambio anche locale + `password_version++` + invalidazione tutti i refresh_tokens
- Se utente non SSO: solo cambio locale (comportamento attuale)
**Acceptance**: cambio password da NIS2 di utente SSO si riflette entro 5 min su TRPG/lg231.
### G04 — Cron sync password
**Già implementato a livello cron host** (vedi `/opt/devenv/scripts/sso-password-sync.sh` e CLAUDE.md). Da verificare/richiedere:
- Sync include il DB `nis2_agile_db.users`
- Match per `sso_identity_id`, compara `password_version`, aggiorna `password_hash` + `password_version` locale
**Action item**: aprire ticket al manutentore agile-services per estendere lo script. **Nessun codice in NIS2**.
**Acceptance**: cambio password da TRPG si propaga a NIS2 in ≤5 min.
---
## 3. Fase 2 — Session management multi-device (P0)
**Obiettivo**: l'utente vede e revoca le sue sessioni attive, come su Google/GitHub.
### G05 — Schema active_sessions
**Migration `docs/sql/016_active_sessions.sql`** (mutuata da TRPG `078_active_sessions.sql`):
```sql
CREATE TABLE IF NOT EXISTS active_sessions (
id CHAR(32) NOT NULL PRIMARY KEY COMMENT 'jti del JWT (bin2hex(random_bytes(16)))',
user_id INT NOT NULL,
organization_id INT NULL COMMENT 'org attiva al momento del login',
ip_address VARCHAR(45) NOT NULL,
user_agent VARCHAR(512) NULL,
device_label VARCHAR(120) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_activity_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP NULL DEFAULT NULL,
revoked_reason ENUM(
'logout','force','password_change','admin','expired_idle','context_switch'
) NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_active (user_id, revoked_at, last_activity_at),
INDEX idx_last_activity (last_activity_at),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE refresh_tokens
ADD COLUMN session_jti CHAR(32) NULL AFTER user_id,
ADD INDEX idx_refresh_jti (session_jti);
```
**Differenza vs TRPG**: aggiunto `organization_id` (NIS2 ha tenant esplicito) e `context_switch` già incluso nell'ENUM dal day 1.
### G06 — Endpoint sessions
Modificare `AuthController` aggiungendo:
- `GET /api/auth/sessions` → lista sessioni attive utente corrente (id, ip, device_label, created_at, last_activity_at, **is_current**)
- `DELETE /api/auth/sessions/{id}` → revoca singola con `revoked_reason='logout'`
- `DELETE /api/auth/sessions` → revoca tutte tranne corrente (cambio password forzato)
- `POST /api/auth/login` modificato: genera `jti = bin2hex(random_bytes(16))`, insert in `active_sessions`, include `jti` nel JWT
- `BaseController::requireAuth()` verifica che `jti` esista in `active_sessions` e non sia revoked
**Acceptance**:
- Login da 3 browser → 3 righe in `active_sessions`
- Revoca sessione X → token X non più valido (verify in `requireAuth`)
- Last activity aggiornato a ogni richiesta autenticata
### G07 — UI Settings: tab Sessioni
Aggiungere a `public/settings.html` tab "Sicurezza" con:
- Lista device (icona, browser, OS, IP, ultima attività, "Questo dispositivo" badge)
- Pulsante "Esci da questo dispositivo" per ogni riga
- Pulsante "Esci da tutti i dispositivi tranne questo" in cima
Pattern UX copiato da TRPG settings (da screenshottare per riferimento, non da analizzare oltre).
---
## 4. Fase 3 — Password reset + Tenant switcher (P0/P1)
### G08 — Forgot/Reset password
**Endpoint** in `AuthController`:
- `POST /api/auth/forgot-password` body `{email}` → genera token (SHA256, expires 30min, salvato in tabella `password_reset_tokens` nuova), invia email con link `https://nis2.agile.software/reset-password.html?token=XXX`
- `POST /api/auth/reset-password` body `{token, new_password}` → verifica token, aggiorna password (+ propaga via SSO se utente federato), invalida tutti i refresh_tokens
**Migration `docs/sql/017_password_reset.sql`**:
```sql
CREATE TABLE password_reset_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
token_hash CHAR(64) NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP NULL,
ip_address VARCHAR(45) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (token_hash),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
**Pagine HTML nuove**:
- `public/forgot-password.html` (form email)
- `public/reset-password.html` (form new password con strength bar, mutuata da TRPG)
**Modifica** `public/login.html`: link "Password dimenticata?" → `forgot-password.html` (rimuove l'alert "contatta presidenza@...").
**Email template**: estendere `EmailService.php` con `sendPasswordResetEmail()`.
**Rate limit**: 3 richieste/ora per IP+email.
### G09 — switchContext endpoint
**Endpoint** `POST /api/auth/switch-context` body `{organization_id}`:
1. Verifica `MembershipService::isMember(userId, orgId)` (o equivalente NIS2 via `user_organizations`)
2. Revoca sessione corrente con `revoked_reason='context_switch'`
3. Crea nuova `active_sessions` row con stesso device, nuovo jti
4. Emette nuovo JWT con `organization_id` claim aggiornato + ruolo org corrispondente
**Acceptance**: il consulente cambia cliente da dropdown → nuovo JWT con nuovo `organization_id` → dashboard riflette il cliente nuovo senza F5 hard.
### G10 — Dropdown tenant in sidebar
Modificare `public/js/common.js`:
- All'avvio: se `currentUser.organizations.length > 1` → mostra dropdown nell'header con nome org corrente + freccia
- Click → modale lista organizations (nome, ruolo, ultimo accesso)
- Click su org → `api.switchContext(orgId)` → ricarica con nuovo token
Pattern visivo allineato a TRPG (icona azienda + nome troncato + chevron).
---
## 5. Fase 4 — Impersonation + Preferences + Versioning (P1/P2)
### G11 — Impersonate
**Endpoint** `POST /api/auth/impersonate` body `{user_id}` (solo super_admin o consulente verso utenti dei clienti del firm):
- Verifica permessi
- Emette JWT con claim aggiuntivo `impersonated_by = currentUserId`
- Logga in `audit_logs` con `severity='warning'`
- TTL JWT ridotto a 1h (no refresh)
**UI**: pulsante in `admin/users.html` ("Entra come questo utente") + banner permanente in tutte le pagine quando si è in modalità impersonate ("Sei loggato come X — esci impersonate").
### G12 — User preferences
**Endpoint** `PUT /api/auth/preferences` per: `preferred_language`, `theme` (chiaro/scuro), `timezone`, `notification_email`, `notification_inapp`.
Separato da `PUT /api/auth/profile` (full_name, phone) per ragioni di audit e granularità permessi futuri.
**Migration `docs/sql/018_user_preferences.sql`**:
```sql
ALTER TABLE users
ADD COLUMN theme ENUM('light','dark','auto') DEFAULT 'auto',
ADD COLUMN timezone VARCHAR(64) DEFAULT 'Europe/Rome',
ADD COLUMN notification_email TINYINT(1) DEFAULT 1,
ADD COLUMN notification_inapp TINYINT(1) DEFAULT 1;
```
### G13 — Versioning UI live
- `public/version.json` → bump a `1.1.0` (con questo allineamento), data corrente, changelog
- `public/js/common.js` → fetch `version.json` all'avvio → mostra in footer sidebar (es. `v1.1.0 (build 20260530)`)
- Tooltip su click apre modale con changelog
### G14 — Cron agent bump PATCH
Coordinarsi con AgentAI (vedi CLAUDE.md sez. AgileHub Agent) per:
- Dopo APPLY_END di un ticket, bumpare `version.json` PATCH (es. 1.1.0 → 1.1.1)
- Log VERSION_BUMP in audit trail standard
**No codice NIS2**: solo ticket di coordinamento.
---
## 6. Fase 5 — Polishing & extras (P2/P3)
### G15 — Demo login
Endpoint dedicato `POST /api/auth/demo-login?scenario=ransomware` che pre-loada un demo user con flag `is_demo=1` e org-clone con dati seed scenari simulazione esistenti (`simulate-nis2.php`).
### G16 — Branding consulente
Nuovo `BrandingController.php` + tabella `firm_branding` (logo_url, primary_color, secondary_color, custom_domain).
Header/sidebar/login leggono branding da `consulting_firm_id` dell'utente corrente.
### G17 — Auth-gate documenti
Copy 1:1 di `app/js/auth-gate.js` da TRPG → `public/js/auth-gate.js`. Includere nei documenti riservati (es. eventuali presentation.html, roadmap.html, listino.html futuri).
### G18 — Refactor controller
Spezzare `AuthController` (~600 righe oggi → 1000+ dopo fasi 1-3) in:
- `AuthController` (login/logout/refresh/sessions)
- `PasswordController` (forgot/reset/change)
- `ProfileController` (me/profile/preferences)
- `SsoController` (impersonate/switchContext/sso-callback)
Estrarre anche `ApiKeyController`, `ConsultingFirmController` se serve (probabilmente sì, in linea con TRPG).
---
## 7. Cosa NON è in scope
- **Porting a Node.js**: NIS2 resta PHP. La frammentazione microservizi di TRPG (`services/auth`, `services/ai`, …) non è obiettivo.
- **2FA/MFA**: rinviato a progetto separato (richiede UX dedicata, TOTP, recovery codes).
- **OAuth2 / social login**: rinviato.
- **WebAuthn / passkey**: rinviato.
---
## 8. Sequenza release e dipendenze
```
Fase 1 (SSO) → Fase 3 (reset password usa SSO propagation)
Fase 1 (SSO) → Fase 4 (impersonate logga in SSO audit)
Fase 2 (sessions) → Fase 3 (switchContext revoca sessione)
Fase 2 (sessions) → Fase 4 (impersonate crea sessione marcata)
Fase 3 (forgot pwd) ⊥ indipendente da Fase 4/5
Fase 4 (versioning) ⊥ indipendente
Fase 5 ⊥ tutto opzionale
```
**Path critico**: Fase 1 → Fase 2 → Fase 3. Le altre possono parallelizzare.
---
## 9. Stima cumulativa
| Fase | Effort | Settimane-uomo |
|------|--------|----------------|
| Fase 1 SSO | M+M+S+S | 1.5 |
| Fase 2 Sessions | M+M+S | 1.0 |
| Fase 3 Reset+Switch+UI | M+M+M | 1.5 |
| Fase 4 Imperson.+Prefs+Ver. | S+S+S+S | 0.8 |
| Fase 5 Demo+Brand+Refactor | M+M+XS+L | 1.5 |
| **Totale** | | **~6 settimane-uomo** |
---
## 10. Decisioni prese (confermate dall'utente 2026-05-29)
| # | Decisione | Scelta | Note operative |
|---|-----------|--------|----------------|
| 1 | SSO_MODE iniziale | **`local`** | Partiamo prudenti. Switch a `dual` dopo validazione (almeno 7 giorni post-Fase 1). |
| 2 | Backfill utenti SSO | **A) Lazy on-login** | Nessuno script massivo. Il link `users.sso_identity_id` viene popolato al primo login post-SSO se l'email matcha `nexus_tenant_db.sso_identities`. Utenti che non loggano restano slegati (accettato). |
| 3 | Sessioni concorrenti per utente | **Nessun limite** | Allineato a TRPG/Google/GitHub. Il consulente lavora da N device, vede tutto in Settings → Sicurezza e revoca a piacere. |
| 4 | TTL token reset password | **30 min** | Token SHA256, single-use (`used_at`), `expires_at = NOW() + 30 min`. Rate limit 3/h per IP+email. |
| 5 | Cadenza version bump | **B) Per fase (MINOR)** | Fine Fase 1 → 1.1.0 · Fine Fase 2 → 1.2.0 · Fine Fase 3 → 1.3.0 · Fine Fase 4 → 1.4.0 · Fine Fase 5 → 1.5.0. PATCH automatico via cron agent attivato in Fase 4 (G14). |
| 6 | Branding white-label (G16) | **B) Mantenuto in Fase 5** | `BrandingController` + tabella `firm_branding` (logo, primary_color, secondary_color, custom_domain) abilitano white-label consulente. Effort M, valore commerciale (upsell). |
---
## 11. Riferimenti
- TRPG schema sessions: `/var/www/trpg-agile/sql/078_active_sessions.sql` (Hetzner, accesso via chiave temp)
- TRPG SsoHelper: `/var/www/trpg-agile/shared/SsoHelper.php`
- TRPG HubJwtVerifier: `/var/www/trpg-agile/shared/HubJwtVerifier.php`
- TRPG AuthController: `/var/www/trpg-agile/services/auth/controllers/AuthController.php` (v1.54.1, 1365 righe)
- Standard SSO suite: `agile-services/docs/SPEC_SSO_SINGLE_SIGN_ON.md` + `ISTRUZIONI_SSO_PRODOTTI.md`
- Cron sync attuale: `agile-services/scripts/sso-password-sync.sh`
- NIS2 schema utenti: `docs/sql/001_initial_schema.sql` (righe 46-80)
---
## 12. Stato esecuzione
### Tutte le 5 fasi — **COMPLETATE 2026-05-29** ✅
| Fase | Esito | Version |
|------|-------|---------|
| **Fase 1 — SSO Federation** | ✅ | 1.1.0 |
| **Fase 2 — Multi-device Sessions** | ✅ | 1.2.0 |
| **Fase 3 — Password Reset + Context Switch** | ✅ | 1.3.0 |
| **Fase 4 — Impersonate + Preferences + Versioning UI** | ✅ | 1.4.0 |
| **Fase 5 — Branding + Auth-gate** | ✅ (G15 e G18 skip) | 1.5.0 |
#### Skip motivati
- **G15 Demo login**: i simulator esistenti (`simulate-nis2.php`, `simulate-nis2-big.php`, `simulate-nis2-b2b.php`) già coprono i flussi demo end-to-end. Un endpoint dedicato non aggiungerebbe valore — i simulator creano org reali con dati seed, esperienza più ricca di un demo-mode singleton.
- **G18 Refactor controllers**: split di `AuthController` in 4 controller separati è investimento tecnico (~5gg) senza valore funzionale immediato. Rinviato a sessione dedicata se diventa necessario per leggibilità.
#### Migrations SQL applicate (DB host Hetzner)
| File | Tabella/Colonne |
|------|-----------------|
| `015_sso_columns.sql` | `users.sso_identity_id`, `users.password_version` |
| `016_active_sessions.sql` | `active_sessions` (jti tracking), `refresh_tokens.session_jti` |
| `017_password_reset.sql` | `password_reset_tokens` |
| `018_user_preferences.sql` | `users.theme/timezone/notif_email/notif_inapp` |
| `019_firm_branding.sql` | `firm_branding` (white-label) |
#### Endpoint API nuovi (dopo Fase 1-5)
```
POST /api/auth/forgot-password (Fase 3 / G08)
POST /api/auth/reset-password (Fase 3 / G08)
POST /api/auth/switchContext (Fase 3 / G09)
POST /api/auth/impersonate (Fase 4 / G11)
GET /api/auth/preferences (Fase 4 / G12)
PUT /api/auth/preferences (Fase 4 / G12)
GET /api/auth/sessions (Fase 2 / G06)
DELETE /api/auth/sessions (Fase 2 / G06)
DELETE /api/auth/sessions/{id} (Fase 2 / G06)
GET /api/branding/current (Fase 5 / G16)
PUT /api/branding (Fase 5 / G16)
```
#### UI changes
- `public/login.html` — link "Password dimenticata?" funzionante
- `public/forgot-password.html` (NUOVO)
- `public/reset-password.html` (NUOVO con strength bar)
- `public/settings.html` — tab Sicurezza con lista sessioni device + revoca
- `public/js/common.js` — sidebar version footer (click → changelog modal), tenant switcher esposto a chi ha ≥2 org, branding CSS vars auto-load
- `public/js/auth-gate.js` (NUOVO) — gate password lato client per documenti riservati
#### File PHP nuovi / modificati
| File | Tipo | Note |
|------|------|------|
| `application/services/SsoHelper.php` | NUOVO | Client SSO dual-mode |
| `application/controllers/BrandingController.php` | NUOVO | White-label firm |
| `application/controllers/AuthController.php` | MODIFICATO | +12 metodi: login (jti+SSO), changePassword, refresh, logout, forgotPassword, resetPassword, switchContext, impersonate, getPreferences, updatePreferences, listSessions, revokeSession, revokeAllSessions |
| `application/controllers/BaseController.php` | MODIFICATO | requireAuth verifica jti + last_activity throttle, parseDeviceLabel, generateRefreshToken con session_jti |
| `application/services/EmailService.php` | MODIFICATO | +sendPasswordReset |
| `application/config/config.php` | MODIFICATO | +RATE_LIMIT_AUTH_FORGOT, +PASSWORD_RESET_TTL_SECONDS |
| `public/index.php` | MODIFICATO | +12 route + branding controller |
#### Comportamento utente attivo OGGI
- SSO **dormiente** (`SSO_MODE=local`) → login esattamente come prima
- Multi-device sessions **ATTIVE** → ogni login crea row in `active_sessions`, JWT senza jti rifiutato
- Password reset **ATTIVO** → link funzionante da `login.html`
- Context switch **ATTIVO** → utenti multi-org vedono dropdown in sidebar
- Impersonate **ATTIVO** → solo super_admin o consulente stesso firm (NB: nessuna UI ancora, solo endpoint)
- Preferences **ATTIVO** → endpoint disponibili (NB: UI tab dedicato non ancora aggiunto)
- Versioning footer **ATTIVO** → versione visibile in sidebar di ogni pagina
- Branding **ATTIVO** → defaults applicati; consulente può PUT branding del suo firm
#### Cron sync SSO (dipendenza esterna)
**AgileHub Ticket #220** aperto a team AGILEHUB per estendere `sso-password-sync.sh` al DB `nis2_agile_db`. Da risolvere prima del switch `SSO_MODE=dual`.
#### Backup completi (rollback ready)
Tutti i file modificati hanno backup `.bak.pre-{fase}-{timestamp}` in:
- `/projects/nis2-agile/application/controllers/`
- `/projects/nis2-agile/public/`
- `/var/www/nis2-agile/.backups/` (Hetzner)
DB backups schema:
- `host_users_pre_017_*.sql`
- `users_schema_pre_015_*.sql`
- `refresh_tokens_pre_016_*.sql`
---
### Fase 1 — SSO Federation: **COMPLETATA 2026-05-29** ✅
| Step | Esito | Note |
|------|-------|------|
| G01 — Migration `015_sso_columns.sql` | ✅ Applicata su prod | Backup `.backups/users_schema_pre_015_20260529-073134.sql`. Idempotenza verificata. 0 utenti in prod (nessun cliente). |
| G02 — `application/services/SsoHelper.php` | ✅ Deployato | Mutuato da TRPG `shared/SsoHelper.php`. Lint OK in container. `getMode()=local`. |
| G03 — `AuthController::login()` + `changePassword()` | ✅ Modificato | Backup `.bak.pre-sso-20260529`. Smoke test 401 OK. Logica dietro `if(!$sso->isLocalOnly())` → no-op con `SSO_MODE=local`. |
| G04 — Vault + `.env` | ✅ Configurato | Vault: `tier1__nis2-app__sso/internal_key` (placeholder). `.env`: SSO_ENDPOINT/SSO_TIMEOUT_MS/SSO_MODE=local. |
| Cron sync (out-of-scope NIS2) | ✅ Ticket aperto | AgileHub Ticket #220 a team AGILEHUB. |
**Comportamento utente**: invariato. Tutte le modifiche sono dietro feature flag `SSO_MODE`. Switch a `dual` previsto dopo almeno 7gg di validazione (decisione §10.1).
### Prossimi step
- **[manutentore]** AgileHub Ticket #220 → estensione `sso-password-sync.sh` al DB `nis2_agile_db`
- **[NIS2]** Quando ticket risolto + 7gg passati → switch `SSO_MODE=local``dual` (modifica `.env` + `docker compose up -d --force-recreate app`)
- **[NIS2]** Recupero valore reale `SSO_INTERNAL_KEY` da Tenant MS prima dello switch e replace in vault con: `docker exec vault-steward node /app/cli/vault-cli.js migrate tier1__nis2-app__sso internal_key '<real>'`
- **[NIS2]** Bump `public/version.json`**1.1.0** (decisione §10.5) a switch confermato
Vedi §3-§6 per Fase 2-5.