Files
breadcrumb-the-shire/docs/explanation-session-management.md
fs 0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00

184 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=<url>`.
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: `core/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: `core/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 | `core/Service/Auth/AuthService.php` |
| RememberMeService | `core/Service/Auth/RememberMeService.php` |
| RememberMeRateLimiter | `core/Service/Auth/RememberMeRateLimiter.php` |
| Session-Settings | `core/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`