Sicherheit: - Bulk-Erstellung serverseitig auf eingeloggte Nutzer beschränkt; expire_minutes wird validiert (anonym: nur Default/Template-Werte, eingeloggt: max. 1 Jahr) - IP-basiertes Rate-Limit über neue Tabelle request_throttle (Voucher-Erstellung + Passwort-Reset-Anfragen), Session-Fallback für Alt-Installationen; Migration 0002 - session_regenerate_id() nach Login, Secure-Cookie-Flag bei HTTPS - Admin-/Aktiv-Status wird pro Request live aus der DB geprüft (Rechteentzug & Deaktivierung wirken sofort); Schutz vor Selbst-Degradierung im Benutzer-Edit - Alle state-ändernden Admin-Aktionen von GET auf POST umgestellt (kein CSRF-Token mehr in URLs) - login_simple.php (Legacy, Debug-Leak) entfernt; cron_test.php nur noch für Admins; .htaccess auf Apache-2.4-Syntax inkl. cron_test.php - M365 Client Secret wird nicht mehr ins Formular zurückgegeben - Updater: Zip-Slip-/Pfad-Traversal-Schutz, Backup vor dem Anwenden mit automatischem Rollback bei Fehlern, AuditLogger-Bug behoben - cron_sync: Token-Vergleich mit hash_equals; login_attempts-Pruning - CSV-Export gegen Excel-Formula-Injection abgesichert Bugfixes: - M365-Login: Fallback auf userPrincipalName, wenn Graph kein 'mail' liefert (Nutzer ohne Exchange-Postfach konnten sich nie anmelden) - PRG-Pattern überall: F5 erzeugt keine Duplikat-Voucher und wiederholt keine Admin-Aktionen (Session-Flash-Messages) - QR-Code nicht mehr invertiert (schwarz auf weiß, scanbar) - Bulk-Erstellung nutzt den UniFi 'n'-Parameter: 1 API-Call statt n× Login + Voucherlisten-Abruf; exaktes Code-Matching per create_time statt "global neuester Voucher" - Mailer: doppelte Zeilenumbrüche behoben, AUTH nur mit Credentials, SMTP-Dot-Stuffing, CLI-sicherer EHLO-Host - forgot_password: System-URL-Auto-Detect (Reset-Link war sonst relativ/kaputt) + Rate-Limit - Audit-Log-Labels an tatsächliche Action-Keys angepasst; Voucher-Erstellung (einzeln & bulk) wird jetzt auditiert - Site-Edit testet die Verbindung auch ohne Passwortänderung UX/UI: - Alert-/Badge-Styles zentral in global.css mit Dark-Mode-Variablen (vorher 7× dupliziert mit hart codierten Hellfarben) - Sticky-Formulare + Tab-Erhalt nach Validierungsfehlern (Bulk), Settings kehren nach dem Speichern zum aktiven Tab zurück - Gültigkeit menschenlesbar (z.B. "8 Stunden" statt "480 Minuten") - Voucher-Name-Default "Gast/Guest" im öffentlichen Modus - Favicon auch auf Login-/öffentlichen Seiten - Verbindungstest-Button pro Site-Karte (Health-Check) - i18n-Pass: Confirm-Dialoge, Toasts, Fehl-/Erfolgsmeldungen in de/en - Sprachumschalter ohne fetch+reload (kein Re-Submit-Dialog) - A11y: Esc schließt Modals, aria-live für Toasts, aria-labels auf Icon-Buttons; APP_KEY-Warnbanner im Dashboard - Dashboard-Sync: set_time_limit passend zur Site-Anzahl; Voucher-Sync mit Map statt SELECT pro Voucher Tooling: - GitHub-Actions-Workflow: PHP-Lint aller Dateien + de/en-Key-Parität https://claude.ai/code/session_01KKVpVPJjrTKGoRgpJcySD4
133 lines
No EOL
5 KiB
PHP
133 lines
No EOL
5 KiB
PHP
<?php
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', 0);
|
||
ini_set('log_errors', 1);
|
||
|
||
require_once __DIR__ . '/config.php';
|
||
require_once __DIR__ . '/includes/Database.php';
|
||
require_once __DIR__ . '/includes/Auth.php';
|
||
|
||
session_start();
|
||
|
||
$db = Database::getInstance();
|
||
$auth = new Auth();
|
||
|
||
// M365 Einstellungen abrufen
|
||
$clientId = $db->getSetting('m365_client_id', '');
|
||
$clientSecret = $db->getSetting('m365_client_secret', '');
|
||
$tenantId = $db->getSetting('m365_tenant_id', '');
|
||
|
||
if (empty($clientId) || empty($clientSecret) || empty($tenantId)) {
|
||
die('Microsoft 365 ist nicht konfiguriert. Bitte kontaktieren Sie Ihren Administrator. <a href="login.php">Zurück zum Login</a>');
|
||
}
|
||
|
||
// Dynamische Redirect URI
|
||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||
$host = $_SERVER['HTTP_HOST'];
|
||
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
|
||
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
|
||
$redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
|
||
|
||
// Fehlerbehandlung
|
||
if (isset($_GET['error'])) {
|
||
$error = htmlspecialchars($_GET['error']);
|
||
$errorDesc = htmlspecialchars($_GET['error_description'] ?? 'Unbekannter Fehler');
|
||
die("Microsoft 365 Login-Fehler: $error<br>$errorDesc<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
|
||
// Authorization Code erhalten
|
||
if (isset($_GET['code'])) {
|
||
// OAuth-State validieren (CSRF-Schutz). Der State wurde in login.php erzeugt
|
||
// und in der Session hinterlegt; er muss exakt zurueckkommen.
|
||
$sessionState = $_SESSION['m365_state'] ?? '';
|
||
$returnedState = $_GET['state'] ?? '';
|
||
unset($_SESSION['m365_state']); // One-Time-Token, nach Pruefung verbrauchen
|
||
|
||
if ($sessionState === '' || !hash_equals($sessionState, $returnedState)) {
|
||
die("Ungültiger oder fehlender Sicherheits-Token (OAuth state). Bitte erneut anmelden.<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
|
||
$code = $_GET['code'];
|
||
|
||
// Token anfordern
|
||
$tokenUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token";
|
||
|
||
$postData = [
|
||
'client_id' => $clientId,
|
||
'client_secret' => $clientSecret,
|
||
'code' => $code,
|
||
'redirect_uri' => $redirectUri,
|
||
'grant_type' => 'authorization_code',
|
||
'scope' => 'openid profile email User.Read'
|
||
];
|
||
|
||
$ch = curl_init($tokenUrl);
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($httpCode !== 200) {
|
||
die("Fehler beim Token-Abruf (HTTP $httpCode): " . htmlspecialchars($response) . "<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
|
||
$tokenData = json_decode($response, true);
|
||
|
||
if (!isset($tokenData['access_token'])) {
|
||
die("Kein Access Token erhalten: " . htmlspecialchars($response) . "<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
|
||
$accessToken = $tokenData['access_token'];
|
||
|
||
// Benutzer-Informationen abrufen
|
||
$userUrl = 'https://graph.microsoft.com/v1.0/me';
|
||
|
||
$ch = curl_init($userUrl);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||
'Authorization: Bearer ' . $accessToken,
|
||
'Content-Type: application/json'
|
||
]);
|
||
|
||
$userResponse = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($httpCode !== 200) {
|
||
die("Fehler beim Abrufen der Benutzer-Daten (HTTP $httpCode): " . htmlspecialchars($userResponse) . "<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
|
||
$userData = json_decode($userResponse, true);
|
||
|
||
// Graph liefert 'mail' nur bei Nutzern mit Exchange-Postfach – fuer alle
|
||
// anderen auf den userPrincipalName zurueckfallen.
|
||
$userEmail = $userData['mail'] ?? $userData['userPrincipalName'] ?? null;
|
||
|
||
if (!isset($userData['id']) || empty($userEmail)) {
|
||
die("Ungültige Benutzer-Daten erhalten: " . htmlspecialchars($userResponse) . "<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
|
||
// Benutzer einloggen oder anlegen
|
||
$microsoftUser = [
|
||
'id' => $userData['id'],
|
||
'email' => $userEmail,
|
||
'name' => $userData['displayName'] ?? trim(($userData['givenName'] ?? '') . ' ' . ($userData['surname'] ?? ''))
|
||
];
|
||
|
||
try {
|
||
$auth->loginWithMicrosoft($microsoftUser);
|
||
header('Location: index.php');
|
||
exit;
|
||
} catch (Exception $e) {
|
||
die("Login-Fehler: " . $e->getMessage() . "<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
|
||
} else {
|
||
// Keine Authorization Code - Redirect zu Microsoft Login
|
||
die("Kein Authorization Code erhalten.<br><a href='login.php'>Zurück zum Login</a>");
|
||
}
|
||
?>
|