diff --git a/docs/explanation-session-management.md b/docs/explanation-session-management.md new file mode 100644 index 0000000..1c2ef4e --- /dev/null +++ b/docs/explanation-session-management.md @@ -0,0 +1,183 @@ +# Session-Management + +Letzte Aktualisierung: 2026-03-14 + +## Ziel + +Vollstaendige Erklaerung des Session-Lifecycle: Wann wird ein User abgemeldet, wann greift der Remember-Token, wie bleiben Frontend und Server synchron. + +## Session-Lifecycle (Uebersicht) + +``` +Login (Passwort / SSO) + │ + ├─ $_SESSION['session_started_at'] = time() + ├─ $_SESSION['session_last_activity'] = time() + └─ Optional: Remember-Token (Cookie, 30 Tage) + │ + ▼ +Jeder Request (web/index.php) + │ + ├─ Session vorhanden? + │ ├─ JA → Idle-Check + Absolute-Check + │ └─ NEIN → Remember-Cookie vorhanden? + │ ├─ JA → Auto-Login (Token validieren + rotieren) + │ └─ NEIN → Redirect /login + │ + ├─ authz_version geaendert? → Permissions neu laden + └─ User deaktiviert/geloescht? → Forced Logout +``` + +## Zwei Timeouts + +| Timeout | Default | Bereich | Zuruecksetzbar? | DB-Key | +|---------|---------|---------|-----------------|--------| +| Idle | 30 Min | 5 Min – 24 Std | Ja, bei jeder Aktivitaet | `session_idle_timeout_minutes` | +| Absolute | 8 Std | 1 Std – 72 Std | Nein, nie | `session_absolute_timeout_hours` | + +Pruefung in `web/index.php` (bei jedem Request): + +``` +Idle: (now - last_activity) > idle_timeout → Timeout +Absolute: (now - session_started_at) > absolute_timeout → Timeout +``` + +Einer greift → `logoutDueToSessionTimeout()` → Audit-Event `auth.session.timeout` → Flash-Message → Redirect `/login?return_to=`. + +Wichtig: Der Absolute-Timeout wird nie zurueckgesetzt, auch nicht durch "Session verlaengern". Er begrenzt die maximale Sitzungsdauer hart. + +## Frontend-Warning und Keepalive + +Quelle: `web/js/components/app-session-warning.js` + +``` +0 Min 28 Min 30 Min +├───────────────┤───────────────┤ + Aktiv Warning-Dialog Timeout + (2 Min Countdown) +``` + +### Interaktions-Tracking + +- Events: `click`, `keydown`, `scroll`, `mousemove`, `touchstart` +- Throttled auf 30s (kein Overhead bei schnellem Scrollen) +- Setzt nur den JS-Timer zurueck, nicht den Server-Timer + +### Background-Keepalive + +Problem ohne Keepalive: User scrollt 29 Min auf einer Seite → JS zeigt "aktiv", Server sagt "idle". + +Loesung: Alle 5 Minuten prueft ein Intervall ob seit dem letzten Sync Interaktion stattfand. Falls ja → `POST /admin/session-ping/data` → Server-Timer zurueckgesetzt. + +| Szenario | Keepalive-Ping? | +|----------|-----------------| +| User aktiv (Maus, Scroll, Klick) | Ja, alle 5 Min | +| User wirklich idle | Nein | +| Warning-Dialog offen | Nein | +| Tab im Hintergrund | Nein (keine Events) | + +### Warning-Dialog + +- Erscheint 2 Min vor Idle-Timeout +- Nur bei Idle-Timeout >= 3 Min +- Kann nicht per ESC oder Backdrop geschlossen werden +- Zwei Optionen: **Session verlaengern** (POST Ping) oder **Abmelden** (Redirect /logout) + +## Remember-Token + +Quelle: `lib/Service/Auth/RememberMeService.php` + +### Token-Struktur + +``` +Cookie "remember" = selector:token + (24 hex) (64 hex) + +DB: user_remember_tokens + ├─ selector → Lookup (UNIQUE Index) + ├─ token_hash → SHA-256 (constant-time Verify) + ├─ expires_at → Default 30 Tage (konfigurierbar 1-365) + └─ last_used +``` + +Selector und Token sind getrennt: Selector fuer schnellen DB-Lookup, Token-Hash fuer timing-sichere Validierung. Kein Klartext-Token in der DB. + +### Auto-Login-Ablauf + +1. Kein aktiver Session-User → Remember-Cookie lesen +2. Rate-Limit pruefen (IP nicht blockiert?) +3. Selector → DB-Lookup +4. Token → `hash_equals(SHA256)` Verify +5. User aktiv? Token nicht abgelaufen? +6. Session neu aufbauen (User, Permissions, Tenant-Kontext) +7. **Token rotieren** (neuer Hash, neues Expiry) +8. Rate-Limit-Counter zuruecksetzen + +Token-Rotation bei jedem Auto-Login begrenzt das Fenster bei Cookie-Diebstahl. + +### Rate Limiting (Remember-Token) + +Quelle: `lib/Service/Auth/RememberMeRateLimiter.php` + +- Storage: Memcached (`Cache::add` + `Cache::increment`) +- Limit: 5 Fehlversuche pro IP in 15 Minuten +- Counter laeuft automatisch ab (Memcached TTL) +- Fail-open: Bei Memcached-Ausfall wird nicht blockiert +- Erfolgreicher Auto-Login setzt Counter zurueck + +Siehe `/docs/howto-anfragelimits.md` fuer alle Rate-Limits. + +## Zusammenspiel Absolute Timeout + Remember-Token + +``` +Login → 8h Absolute Timeout → Session endet + │ + Remember-Token? + ├─ JA → Neue Session (neuer 8h-Timer!) + └─ NEIN → Login-Seite +``` + +Solange der Remember-Token lebt (Default 30 Tage), bekommt der User nach Ablauf des Absolute-Timeout automatisch eine neue Session. Der Uebergang ist nahtlos (kein Flash, kein Login-Screen). + +Das bedeutet: Mit aktivem Remember-Token ist die effektive Session-Dauer nicht 8 Stunden, sondern bis zu 30 Tage. Das ist by-design, sollte aber bei Compliance-Anforderungen (z. B. echte Re-Authentifizierung nach X Stunden) beruecksichtigt werden. + +## Cookie-Attribute + +| Attribut | Wert | Grund | +|----------|------|-------| +| `httponly` | `true` | Kein JS-Zugriff (XSS-Schutz) | +| `samesite` | `Lax` | CSRF-Schutz bei Cross-Site-Navigation | +| `secure` | dynamisch | `true` bei HTTPS, `false` bei HTTP (Dev) | +| `path` | `/` | Gueltig fuer gesamte Domain | + +Das Secure-Flag wird automatisch anhand des Protokolls gesetzt (`RequestRuntime::isSecure()`). In Produktion muss HTTPS ueber Nginx erzwungen werden (Redirect 80→443 + HSTS). + +## Audit-Events + +| Event | Ausloeser | +|-------|-----------| +| `auth.login.success` | Erfolgreicher Login (Passwort/SSO) | +| `auth.login.failed` | Fehlgeschlagener Login | +| `auth.logout` | Manueller Logout | +| `auth.session.timeout` | Idle- oder Absolute-Timeout | + +## Beteiligte Dateien + +| Komponente | Pfad | +|------------|------| +| Timeout-Pruefung | `web/index.php` | +| AuthService | `lib/Service/Auth/AuthService.php` | +| RememberMeService | `lib/Service/Auth/RememberMeService.php` | +| RememberMeRateLimiter | `lib/Service/Auth/RememberMeRateLimiter.php` | +| Session-Settings | `lib/Service/Settings/SettingsSessionGateway.php` | +| Frontend-Warning | `web/js/components/app-session-warning.js` | +| Session-Ping | `pages/admin/session-ping/data().php` | +| Login-Action | `pages/auth/login().php` | +| SSO-Callback | `pages/auth/microsoft/callback().php` | + +## Vertiefung + +- `/docs/explanation-sicherheitsmodell.md` +- `/docs/howto-anfragelimits.md` +- `/docs/howto-auth-sso-smoke-test.md` +- `/docs/reference-konfiguration.md` diff --git a/docs/explanation-sicherheitsmodell.md b/docs/explanation-sicherheitsmodell.md index 7339502..8882bea 100644 --- a/docs/explanation-sicherheitsmodell.md +++ b/docs/explanation-sicherheitsmodell.md @@ -108,6 +108,7 @@ API-Sonderfall: ## Vertiefung +- `/docs/explanation-session-management.md` - `/docs/howto-anfragelimits.md` - `/docs/tutorial-07-security-tenant-scope.md` - `/docs/howto-rbac-permissions-playbook.md` diff --git a/docs/howto-anfragelimits.md b/docs/howto-anfragelimits.md index e6f30b3..b76fa53 100644 --- a/docs/howto-anfragelimits.md +++ b/docs/howto-anfragelimits.md @@ -1,6 +1,6 @@ # Anfragelimits (Rate Limiting) -Letzte Aktualisierung: 2026-03-06 +Letzte Aktualisierung: 2026-03-14 ## Ziel @@ -61,8 +61,21 @@ In `pages/api/v1/auth/login().php` (zusätzlich zu globalen API-Limits): Erfolgreicher API-Login setzt `api.auth.login.email_ip` zurück. +## Remember-Token-Limits + +In `lib/Service/Auth/RememberMeRateLimiter.php` (Memcached-basiert): + +1. Pro IP (`remember_rl:{ip}`) + - 5 Fehlversuche / 900s + - Block 900s (automatisch via Memcached-TTL) + +Erfolgreicher Auto-Login setzt den Counter zurück. + +Unterschied zu den DB-basierten Limits oben: Memcached statt `request_rate_limits`-Tabelle. Verhindert DB-Last bei Brute-Force gegen Remember-Cookies. + ## Smoke-Test 1. API ohne Token >120 Requests/min gegen `/api/v1/*` -> `429` erwartet. 2. Web-Login mit falschem Passwort >5 pro E-Mail+IP -> `429` erwartet. 3. API-Login mit falschen Credentials wiederholt -> `429` erwartet. +4. Remember-Cookie mit falschem Token >5 pro IP -> Auto-Login blockiert, Cookie geloescht. diff --git a/docs/index.md b/docs/index.md index be5c8f5..b427f3b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,6 +66,8 @@ Diese Dokumentation ist nach dem Diataxis-Modell in vier Quadranten gegliedert: - Wie der AppContainer funktioniert und neue Services registriert werden. - `/docs/explanation-sicherheitsmodell.md` - Vollstaendiges Sicherheitsmodell (Auth, Permission, Scope). +- `/docs/explanation-session-management.md` + - Session-Lifecycle, Timeouts, Remember-Token, Frontend-Keepalive. - `/docs/explanation-einstellungen-speicherung.md` - Warum Settings in DB + Datei-Cache koexistieren. - `/docs/explanation-docker-betrieb.md`