From 182a87978e53744624383831b266e4e0735f6338 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 20 Aug 2026 00:15:22 +0200 Subject: [PATCH] Auth-Umschalter: Session.php verzweigt auf AUTH_BACKEND (jwt = JWT-Access-Cookie + widerrufbare Redis-Session) - teacher/admin Login/Read/Logout branchen auf AUTH_BACKEND; session-Pfad unveraendert (Live-Default, prod-sicher). - jwt: loginTeacher/loginAdmin legen Redis-Session (SessionStore) an + setzen kurzlebiges JWT-Cookie (ggs_t/ggs_a) + langes Sid-Cookie (ggs_ts/ggs_as). - Read: Fast-Path via signiertem JWT (zustandslos), Refresh-Path via Sid->Redis + frisches JWT; Widerruf = Redis DEL bei Logout. - Teacher/Admin getrennte Cookies (koexistieren), typ-Check gegen Rollenverwechslung. Lazy-require von Jwt/SessionStore (jeder Einstiegspfad). - Schueler unveraendert (UUID-Cookie + DB, schon pod-unabhaengig). Co-Authored-By: Claude Opus 4.8 --- App/php/lib/Session.php | 135 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 123 insertions(+), 12 deletions(-) diff --git a/App/php/lib/Session.php b/App/php/lib/Session.php index c104abd..55adea3 100644 --- a/App/php/lib/Session.php +++ b/App/php/lib/Session.php @@ -1,8 +1,14 @@ $expires, + 'path' => (defined('BASE_PATH') ? BASE_PATH : '') . '/', + 'samesite' => 'Lax', + 'secure' => defined('IS_PRODUCTION') ? IS_PRODUCTION : false, + 'httponly' => true, + ]); + $_COOKIE[$name] = $val; // im selben Request sofort sichtbar + } + + private static function dropCookie(string $name): void { + setcookie($name, '', [ + 'expires' => 1, + 'path' => (defined('BASE_PATH') ? BASE_PATH : '') . '/', + 'samesite' => 'Lax', + 'secure' => defined('IS_PRODUCTION') ? IS_PRODUCTION : false, + 'httponly' => true, + ]); + unset($_COOKIE[$name]); + } + + /** jwt-Login: Redis-Session anlegen + Sid-Cookie (lang) + Access-JWT (kurz). */ + private static function jwtLogin(string $role, int $sub): void { + self::jwtBoot(); + [$acc, $sidC, $typ] = self::roleCookies($role); + $sid = SessionStore::create(['typ' => $typ, 'sub' => $sub]); + self::putCookie($sidC, $sid, time() + (defined('JWT_REFRESH_TTL') ? (int)JWT_REFRESH_TTL : 2592000)); + self::putCookie($acc, Jwt::issue(['sub' => $sub, 'typ' => $typ, 'sid' => $sid]), + time() + (defined('JWT_TTL') ? (int)JWT_TTL : 900)); + } + + /** jwt-Read: aktuelle User-ID fuer Rolle (oder null). + * Fast-Path (gueltiges Access-Token) ist zustandslos ohne Redis-Hit; + * erst wenn das kurzlebige Token abgelaufen ist, wird ueber das Sid- + * Cookie die widerrufbare Redis-Session geprueft und ein frisches + * Access-Token nachgelegt. */ + private static function jwtUserId(string $role): ?int { + self::jwtBoot(); + [$acc, $sidC, $typ] = self::roleCookies($role); + + // Fast-Path: gueltiges, signiertes Access-Token → kein Redis-Hit. + $tok = $_COOKIE[$acc] ?? null; + if ($tok) { + $c = Jwt::verify($tok); + if ($c && ($c['typ'] ?? '') === $typ && isset($c['sub'])) { + return (int) $c['sub']; + } + } + + // Refresh-Path: Access-Token fehlt/abgelaufen → Sid-Cookie → Redis. + $sid = $_COOKIE[$sidC] ?? null; + if (!$sid) return null; + try { + $data = SessionStore::get($sid); // slidet TTL; null = widerrufen/abgelaufen + } catch (\Throwable $e) { + return null; // Redis nicht erreichbar → wie „nicht eingeloggt" + } + if (!$data || ($data['typ'] ?? '') !== $typ || !isset($data['sub'])) return null; + + // frisches Access-Token nachlegen (nur solange keine Header raus sind) + if (!headers_sent()) { + self::putCookie($acc, Jwt::issue(['sub' => (int)$data['sub'], 'typ' => $typ, 'sid' => $sid]), + time() + (defined('JWT_TTL') ? (int)JWT_TTL : 900)); + } + return (int) $data['sub']; + } + + /** jwt-Logout: Redis-Session widerrufen + Cookies loeschen. */ + private static function jwtLogout(string $role): void { + self::jwtBoot(); + [$acc, $sidC, ] = self::roleCookies($role); + $sid = $_COOKIE[$sidC] ?? null; + if ($sid) { try { SessionStore::revoke($sid); } catch (\Throwable $e) {} } + self::dropCookie($acc); + self::dropCookie($sidC); + } + /** Schueler-Session-ID aus Cookie */ public static function studentId(): ?string { return $_COOKIE[self::COOKIE_NAME] ?? null; @@ -69,12 +177,14 @@ class Session { /** Lehrer eingeloggt? (startet die Session lazy — Aufrufer müssen nicht) */ public static function teacherId(): ?int { + if (self::jwtMode()) return self::jwtUserId('teacher'); self::start(); return $_SESSION['teacher_id'] ?? null; } /** Lehrer-Login */ public static function loginTeacher(int $teacherId): void { + if (self::jwtMode()) { self::jwtLogin('teacher', $teacherId); return; } self::start(); $_SESSION['teacher_id'] = $teacherId; } @@ -91,23 +201,26 @@ class Session { } // ── Admin (Super-Admin nach 2FA) ──────────────────────────────────── - // Einheitlicher Zugang wie beim Lehrer, damit der spätere JWT+Redis- - // Umschalter (AUTH_BACKEND) an EINER Stelle greift statt an jedem Endpoint. + // Einheitlicher Zugang wie beim Lehrer, damit der JWT+Redis-Umschalter + // (AUTH_BACKEND) an EINER Stelle greift statt an jedem Endpoint. /** Admin eingeloggt (2FA abgeschlossen)? */ public static function adminId(): ?int { + if (self::jwtMode()) return self::jwtUserId('admin'); self::start(); return $_SESSION['admin_id'] ?? null; } /** Admin-Login abschließen (nach erfolgreicher 2FA). */ public static function loginAdmin(int $adminId): void { + if (self::jwtMode()) { self::jwtLogin('admin', $adminId); return; } self::start(); $_SESSION['admin_id'] = $adminId; } - /** Admin-Logout — inkl. offener 2FA-Phase (admin_pending). */ + /** Admin-Logout — inkl. offener 2FA-Phase (admin_pending liegt immer nativ). */ public static function logoutAdmin(): void { + if (self::jwtMode()) self::jwtLogout('admin'); self::start(); unset($_SESSION['admin_id'], $_SESSION['admin_pending']); } @@ -124,14 +237,12 @@ class Session { } public static function logout(): void { + if (self::jwtMode()) { + self::jwtLogout('teacher'); + self::jwtLogout('admin'); + } self::start(); session_destroy(); - setcookie(self::COOKIE_NAME, '', [ - 'expires' => 1, - 'path' => BASE_PATH . '/', - 'samesite' => 'Lax', - 'secure' => IS_PRODUCTION, - 'httponly' => true, - ]); + self::dropCookie(self::COOKIE_NAME); } }