diff --git a/backend/prisma/migrations/20260818120000_audit_token_refresh_action/migration.sql b/backend/prisma/migrations/20260818120000_audit_token_refresh_action/migration.sql new file mode 100644 index 00000000..d0b2fa09 --- /dev/null +++ b/backend/prisma/migrations/20260818120000_audit_token_refresh_action/migration.sql @@ -0,0 +1,8 @@ +-- Neuer AuditAction-Wert TOKEN_REFRESH: trennt stille Access-Token-Refreshes +-- vom interaktiven Login-Rauschen (eigene Action + LOW-Sensitivität). +-- MODIFY COLUMN ist idempotent (setzt die Enum-Definition, mehrfach ausführbar). +ALTER TABLE `AuditLog` + MODIFY COLUMN `action` ENUM( + 'CREATE','READ','UPDATE','DELETE','EXPORT','ANONYMIZE', + 'LOGIN','LOGOUT','LOGIN_FAILED','TOKEN_REFRESH' + ) NOT NULL; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index d4425ebc..7a0d5255 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1231,6 +1231,7 @@ enum AuditAction { LOGIN LOGOUT LOGIN_FAILED + TOKEN_REFRESH // stiller Access-Token-Refresh (Cookie), kein interaktiver Login } enum AuditSensitivity { diff --git a/backend/src/middleware/audit.ts b/backend/src/middleware/audit.ts index 694147fb..e6d3c023 100644 --- a/backend/src/middleware/audit.ts +++ b/backend/src/middleware/audit.ts @@ -49,6 +49,10 @@ function determineAction(method: string, path: string, success: boolean): AuditA if (path.includes('/auth/logout')) { return 'LOGOUT'; } + // Stiller Token-Refresh (Cookie) – kein interaktiver Login, eigene Action + if (path.includes('/auth/refresh')) { + return 'TOKEN_REFRESH'; + } // Standard CRUD-Aktionen switch (method.toUpperCase()) { @@ -157,6 +161,7 @@ const ACTION_LABELS: Record = { LOGIN: 'angemeldet', LOGOUT: 'abgemeldet', LOGIN_FAILED: 'Anmeldung fehlgeschlagen', + TOKEN_REFRESH: 'Sitzung verlängert', }; /** @@ -198,6 +203,7 @@ function generateHumanLabel( : `Anmeldung fehlgeschlagen für ${email}`; } if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet'; + if (path.includes('/auth/refresh')) return 'Sitzung verlängert (Token erneuert)'; // Kunden-Operationen if (resourceType === 'Customer') { @@ -436,6 +442,9 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct customerId: req.user?.customerId, isCustomerPortal: req.user?.isCustomerPortal, action, + // Stiller Token-Refresh ist Routine → LOW statt CRITICAL (sonst Log-Flut). + // Andere Auth-Events behalten ihre Default-Sensitivität (Authentication → CRITICAL). + sensitivity: action === 'TOKEN_REFRESH' ? 'LOW' : undefined, resourceType: mapping.type, resourceId, resourceLabel, diff --git a/docs/todo.md b/docs/todo.md index bcc0f723..e3cf0236 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🔇 Audit-Log: stiller Token-Refresh entrauscht (`TOKEN_REFRESH`)** (2026-08-18) + - Automatische `POST /auth/refresh`-Aufrufe (Interceptor bei 401 / nach + Seiten-Reload, da Access-Token nur im Speicher) wurden als `CREATE` / + „Anmeldung erstellt" / **CRITICAL** / User `anonymous` geloggt → sah aus wie + anonyme Login-Flut, war aber die eigene Session. + - Neuer `AuditAction`-Wert **`TOKEN_REFRESH`** (Enum-Migration + `20260818120000_audit_token_refresh_action`, idempotentes `MODIFY COLUMN`). + `determineAction()` erkennt `/auth/refresh` → eigene Action; Label + „Sitzung verlängert (Token erneuert)"; Sensitivität in der Middleware + explizit auf **LOW** (statt Default `Authentication → CRITICAL`). + - Frontend `AuditLogs.tsx`: Filter-Option „Sitzung verlängert" + dezente + Badge-Farbe (slate); Typ-Union ergänzt. `anonymous` bleibt (Endpoint läuft + ohne `authenticate`-Middleware, authentifiziert per Cookie im Service) – + bewusst nicht geändert (Option 1). + - Verifiziert: Migration auf Dev-DB aktiv, `tsc` + `vite build` grün. + - [x] **🔒 Mass-Assignment-Schutz: Nested-Vertragsdetails (Pentest R162-01)** (2026-08-18) - `createContract`/`updateContract` spreadeten `energyDetails`/`tvDetails`/ `carInsuranceDetails`/`mobileDetails` (via `...mobileData`) roh an Prisma → diff --git a/frontend/src/pages/settings/AuditLogs.tsx b/frontend/src/pages/settings/AuditLogs.tsx index 8cfee1d3..a0779dc0 100644 --- a/frontend/src/pages/settings/AuditLogs.tsx +++ b/frontend/src/pages/settings/AuditLogs.tsx @@ -20,6 +20,7 @@ const ACTION_OPTIONS = [ { value: 'LOGIN', label: 'Login' }, { value: 'LOGOUT', label: 'Logout' }, { value: 'LOGIN_FAILED', label: 'Login fehlgeschlagen' }, + { value: 'TOKEN_REFRESH', label: 'Sitzung verlängert' }, ]; const SENSITIVITY_OPTIONS = [ @@ -68,6 +69,7 @@ function getActionColor(action: AuditAction): string { case 'LOGIN': return 'bg-teal-100 text-teal-800'; case 'LOGOUT': return 'bg-gray-100 text-gray-800'; case 'LOGIN_FAILED': return 'bg-red-200 text-red-900'; + case 'TOKEN_REFRESH': return 'bg-slate-50 text-slate-500'; default: return 'bg-gray-100 text-gray-800'; } } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 2367477a..1ccea4d9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -877,7 +877,7 @@ export interface CockpitResult { // ==================== AUDIT-LOGGING & DSGVO ==================== -export type AuditAction = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE' | 'EXPORT' | 'ANONYMIZE' | 'LOGIN' | 'LOGOUT' | 'LOGIN_FAILED'; +export type AuditAction = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE' | 'EXPORT' | 'ANONYMIZE' | 'LOGIN' | 'LOGOUT' | 'LOGIN_FAILED' | 'TOKEN_REFRESH'; export type AuditSensitivity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; export type ConsentType = 'DATA_PROCESSING' | 'MARKETING_EMAIL' | 'MARKETING_PHONE' | 'DATA_SHARING_PARTNER'; export type ConsentStatus = 'GRANTED' | 'WITHDRAWN' | 'PENDING';