diff --git a/auth/auth.php b/auth/auth.php index bd3ca1d..c647621 100644 --- a/auth/auth.php +++ b/auth/auth.php @@ -1,270 +1,344 @@ +// +//
Ahoj,
+// +// +// Typické použití – stránka s omezeným přístupem +// (VYZADOVAT_PRIHLASENI = false v config.php): +// +// +// +//Přihlášený obsah.
+// +// +//Veřejný obsah. Chceš víc? Přihlas se:
+// +// +// // ============================================================ -// Zpracovává přihlášení z dvou zdrojů: -// 1) Vlastní formulář na této stránce (login.php) -// 2) Formulář $auth_login_html vložený na jiné stránce -// (v tom případě přijde v POST i redirect_url) require_once __DIR__ . '/config.php'; require_once __DIR__ . '/db.php'; +// ------------------------------------------------------------ +// SPUŠTĚNÍ SESSION +// ------------------------------------------------------------ + +// Nastavíme parametry session cookie ještě PŘED session_start(). +// HttpOnly = JavaScript nemůže cookie číst (ochrana před XSS) +// SameSite = cookie se neposílá při požadavcích z jiných webů +// (ochrana před CSRF) +// Secure = cookie se posílá jen přes HTTPS (pokud web běží na HTTPS) session_name(SESSION_NAZEV); session_set_cookie_params([ - 'lifetime' => 0, + 'lifetime' => 0, // 0 = cookie platí do zavření prohlížeče + // (trvalost zajišťuje remember me, ne session) 'path' => '/', - 'httponly' => true, - 'samesite' => 'Strict', - // 'secure' => true, + 'httponly' => true, // JavaScript k cookie nemá přístup + 'samesite' => 'Strict', // ochrana před CSRF + // 'secure' => true, // odkomentuj pokud web běží na HTTPS ]); session_start(); -// Pokud je uživatel již přihlášen, přesměrujeme ho rovnou dál -if (isset($_SESSION['uzivatel_id'])) { - header('Location: ' . AUTH_REDIRECT_PO_PRIHLASENI); +// ------------------------------------------------------------ +// VÝCHOZÍ STAV – uživatel není přihlášen +// ------------------------------------------------------------ +// Tyto hodnoty jsou nastaveny vždy – při každém načtení stránky. +// Teprve níže se případně přepíší na true / skutečné hodnoty. + +$auth_prihlasen = false; +$auth_uzivatel = [ + 'id' => null, + 'email' => null, + 'admin' => false, +]; +$auth_logout_html = ''; +$auth_login_html = ''; + +// ------------------------------------------------------------ +// KROK 1: Existuje platná session? +// ------------------------------------------------------------ +// Session je nejrychlejší způsob ověření – nevyžaduje dotaz do DB. +// Session data jsou uložena na serveru, uživatel je nemůže zfalšovat. + +if (isset($_SESSION['uzivatel_id']) && isset($_SESSION['email'])) { + + // Kontrola expirace session (nečinnost) + if (isset($_SESSION['posledni_aktivita']) && + (time() - $_SESSION['posledni_aktivita']) > SESSION_EXPIRACE) { + + // Session vypršela – zrušíme ji + $_SESSION = []; + session_destroy(); + + } else { + + // Session je platná – uživatel je přihlášen + $auth_prihlasen = true; + $auth_uzivatel = [ + 'id' => $_SESSION['uzivatel_id'], + 'email' => $_SESSION['email'], + // Přetypování na bool – ochrana pro případ, že by v session + // byla jiná hodnota než true/false + 'admin' => (bool) $_SESSION['admin'], + ]; + + // Aktualizujeme čas poslední aktivity + $_SESSION['posledni_aktivita'] = time(); + + } +} + +// ------------------------------------------------------------ +// KROK 2: Session neexistuje – zkusíme remember me cookie +// ------------------------------------------------------------ +// Cookie obsahuje pouze selector a token. Nikdy neobsahuje +// heslo, ID uživatele ani příznak admin. Vše citlivé je na serveru. + +if (!$auth_prihlasen && + isset($_COOKIE['auth_selector']) && + isset($_COOKIE['auth_token'])) { + + $cookie_selector = $_COOKIE['auth_selector']; + $cookie_token = $_COOKIE['auth_token']; + + // Vyhledáme záznam v DB podle selectoru (přesná shoda, rychlé) + $stmt = $pdo->prepare(" + SELECT + t.id AS token_id, + t.uzivatel_id, + t.token_hash, + t.expiruje, + u.email, + u.admin + FROM `" . DB_TABULKA_TOKENY . "` t + JOIN `" . DB_TABULKA_UZIVATELE . "` u ON u.id = t.uzivatel_id + WHERE t.selector = :selector + LIMIT 1 + "); + $stmt->execute([':selector' => $cookie_selector]); + $zaznam = $stmt->fetch(); + + $cookie_ok = false; + + if ($zaznam) { + + // Zkontrolujeme, zda token ještě nevypršel + if (time() < strtotime($zaznam['expiruje'])) { + + // Ověříme tajný token pomocí password_verify(). + // Tato funkce záměrně trvá stejně dlouho bez ohledu + // na výsledek – chrání před timing útoky. + if (password_verify($cookie_token, $zaznam['token_hash'])) { + $cookie_ok = true; + } + } + } + + if ($cookie_ok) { + + // Cookie je platná – obnovíme session + // Regenerujeme session ID (ochrana před session fixation) + session_regenerate_id(true); + + $_SESSION['uzivatel_id'] = $zaznam['uzivatel_id']; + $_SESSION['email'] = $zaznam['email']; + $_SESSION['admin'] = (bool) $zaznam['admin']; + $_SESSION['posledni_aktivita'] = time(); + + // Vygenerujeme CSRF token pokud ještě neexistuje + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + + $auth_prihlasen = true; + $auth_uzivatel = [ + 'id' => $zaznam['uzivatel_id'], + 'email' => $zaznam['email'], + 'admin' => (bool) $zaznam['admin'], + ]; + + // Prodloužíme platnost tokenu v DB i v cookie + $nova_expirace = time() + REMEMBER_EXPIRACE; + $nova_expirace_dt = date('Y-m-d H:i:s', $nova_expirace); + + $stmt2 = $pdo->prepare(" + UPDATE `" . DB_TABULKA_TOKENY . "` + SET `expiruje` = :expiruje + WHERE `id` = :id + "); + $stmt2->execute([ + ':expiruje' => $nova_expirace_dt, + ':id' => $zaznam['token_id'], + ]); + + // Prodloužíme cookie v prohlížeči + setcookie('auth_selector', $cookie_selector, [ + 'expires' => $nova_expirace, + 'path' => '/', + 'httponly' => true, + 'samesite' => 'Strict', + // 'secure' => true, + ]); + setcookie('auth_token', $cookie_token, [ + 'expires' => $nova_expirace, + 'path' => '/', + 'httponly' => true, + 'samesite' => 'Strict', + // 'secure' => true, + ]); + + // Smažeme staré (expirované) tokeny z DB příležitostně + $stmt3 = $pdo->prepare(" + DELETE FROM `" . DB_TABULKA_TOKENY . "` + WHERE `expiruje` < :ted + "); + $stmt3->execute([':ted' => date('Y-m-d H:i:s')]); + + } else { + + // Cookie je neplatná nebo expirovaná – smažeme ji + if ($zaznam) { + $stmt4 = $pdo->prepare(" + DELETE FROM `" . DB_TABULKA_TOKENY . "` + WHERE `id` = :id + "); + $stmt4->execute([':id' => $zaznam['token_id']]); + } + + setcookie('auth_selector', '', [ + 'expires' => time() - 3600, + 'path' => '/', + 'httponly' => true, + 'samesite' => 'Strict', + ]); + setcookie('auth_token', '', [ + 'expires' => time() - 3600, + 'path' => '/', + 'httponly' => true, + 'samesite' => 'Strict', + ]); + } +} + +// ------------------------------------------------------------ +// KROK 3: Rozhodnutí – přesměrovat nebo pokračovat? +// ------------------------------------------------------------ + +if (!$auth_prihlasen && VYZADOVAT_PRIHLASENI) { + + // Uložíme URL aktuální stránky pro přesměrování po přihlášení + $aktualni_url = $_SERVER['REQUEST_URI'] ?? ''; + if (!empty($aktualni_url)) { + $_SESSION['redirect_po_prihlaseni'] = $aktualni_url; + } + + header('Location: ' . AUTH_LOGIN_URL); exit; } // ------------------------------------------------------------ -// CSRF TOKEN +// CSRF TOKEN – zajistíme, že vždy existuje // ------------------------------------------------------------ +// Je potřebný pro oba HTML formuláře níže. if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } -$csrf_token = $_SESSION['csrf_token']; // ------------------------------------------------------------ -// ZPRACOVÁNÍ FORMULÁŘE +// PŘIPRAVENÉ HTML PROMĚNNÉ // ------------------------------------------------------------ -$chyba = ''; -$email_hodnota = ''; +// -- Odhlašovací formulář ------------------------------------ +// Neprázdný pouze pokud je uživatel přihlášen. +// Použití na stránce: echo $auth_logout_html; -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - - // -- Ověření CSRF tokenu ---------------------------------- - $csrf_z_formulare = $_POST['csrf_token'] ?? ''; - if (!hash_equals($csrf_token, $csrf_z_formulare)) { - $chyba = 'Neplatný požadavek. Zkuste stránku obnovit a přihlásit se znovu.'; - } - - if (empty($chyba)) { - - $email = trim($_POST['email'] ?? ''); - $heslo = $_POST['heslo'] ?? ''; - $zapamatovat = isset($_POST['zapamatovat']); - - $email_hodnota = htmlspecialchars($email); - - if (empty($email) || empty($heslo)) { - $chyba = 'Zadejte email a heslo.'; - } - } - - if (empty($chyba)) { - - // -- Kontrola brute force ----------------------------- - $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; - $okno_od = date('Y-m-d H:i:s', time() - BRUTE_OKNO); - - $stmt = $pdo->prepare(" - SELECT COUNT(*) AS pocet - FROM `" . DB_TABULKA_BRUTE . "` - WHERE (`ip_adresa` = :ip OR `email` = :email) - AND `cas` > :okno_od - "); - $stmt->execute([ - ':ip' => $ip, - ':email' => $email, - ':okno_od' => $okno_od, - ]); - $pocet_pokusu = $stmt->fetch()['pocet']; - - if ($pocet_pokusu >= BRUTE_MAX_POKUSU) { - $chyba = 'Příliš mnoho neúspěšných pokusů. Zkuste to prosím za chvíli.'; - } - } - - if (empty($chyba)) { - - // -- Ověření emailu a hesla v DB ---------------------- - $stmt = $pdo->prepare(" - SELECT `id`, `email`, `heslo`, `admin` - FROM `" . DB_TABULKA_UZIVATELE . "` - WHERE `email` = :email - LIMIT 1 - "); - $stmt->execute([':email' => $email]); - $uzivatel = $stmt->fetch(); - - // I když uživatel neexistuje, zavoláme password_verify na fiktivní - // hash – aby útočník nemohl podle doby odpovědi poznat, zda email existuje - $fiktivni_hash = '$2y$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ01234'; - $hash_k_overeni = $uzivatel ? $uzivatel['heslo'] : $fiktivni_hash; - $heslo_ok = password_verify($heslo, $hash_k_overeni); - - if (!$uzivatel || !$heslo_ok) { - - // Zapíšeme neúspěšný pokus - $stmt2 = $pdo->prepare(" - INSERT INTO `" . DB_TABULKA_BRUTE . "` - (`ip_adresa`, `email`) - VALUES - (:ip, :email) - "); - $stmt2->execute([ - ':ip' => $ip, - ':email' => $email, - ]); - - $chyba = 'Nesprávný email nebo heslo.'; - - } else { - - // -- Přihlášení úspěšné --------------------------- - session_regenerate_id(true); - - $_SESSION['uzivatel_id'] = $uzivatel['id']; - $_SESSION['email'] = $uzivatel['email']; - $_SESSION['admin'] = (bool) $uzivatel['admin']; - $_SESSION['posledni_aktivita'] = time(); - $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); - - // -- Remember me ---------------------------------- - if ($zapamatovat) { - - $selector = bin2hex(random_bytes(16)); - $token = bin2hex(random_bytes(32)); - $token_hash = password_hash($token, PASSWORD_DEFAULT); - $expiruje = date('Y-m-d H:i:s', time() + REMEMBER_EXPIRACE); - - $stmt3 = $pdo->prepare(" - INSERT INTO `" . DB_TABULKA_TOKENY . "` - (`uzivatel_id`, `selector`, `token_hash`, `expiruje`) - VALUES - (:uzivatel_id, :selector, :token_hash, :expiruje) - "); - $stmt3->execute([ - ':uzivatel_id' => $uzivatel['id'], - ':selector' => $selector, - ':token_hash' => $token_hash, - ':expiruje' => $expiruje, - ]); - - $cookie_expirace = time() + REMEMBER_EXPIRACE; - setcookie('auth_selector', $selector, [ - 'expires' => $cookie_expirace, - 'path' => '/', - 'httponly' => true, - 'samesite' => 'Strict', - // 'secure' => true, - ]); - setcookie('auth_token', $token, [ - 'expires' => $cookie_expirace, - 'path' => '/', - 'httponly' => true, - 'samesite' => 'Strict', - // 'secure' => true, - ]); - } - - // -- Přesměrování po přihlášení ------------------- - // Priorita zdrojů pro redirect URL: - // 1) redirect_url z POST (formulář byl na jiné stránce) - // 2) redirect_po_prihlaseni v session (uloženo v auth.php) - // 3) výchozí stránka z config.php - $redirect_z_post = trim($_POST['redirect_url'] ?? ''); - $redirect_ze_session = $_SESSION['redirect_po_prihlaseni'] ?? ''; - - // Bezpečnostní kontrola redirect URL: - // Povolujeme pouze relativní URL začínající lomítkem, - // aby útočník nemohl přesměrovat na cizí web - // (tzv. open redirect útok) - if (!empty($redirect_z_post) && str_starts_with($redirect_z_post, '/')) { - $redirect = $redirect_z_post; - } elseif (!empty($redirect_ze_session) && str_starts_with($redirect_ze_session, '/')) { - $redirect = $redirect_ze_session; - } else { - $redirect = AUTH_REDIRECT_PO_PRIHLASENI; - } - - unset($_SESSION['redirect_po_prihlaseni']); - - header('Location: ' . $redirect); - exit; - } - } +if ($auth_prihlasen) { + $auth_logout_html = + ''; } -// ------------------------------------------------------------ -// HTML VÝSTUP -// ------------------------------------------------------------ -?> - - - - -Chyba:
- + // Aktuální URL předáme login.php jako parametr, + // aby nás po úspěšném přihlášení přesměroval zpět sem + $aktualni_url = $_SERVER['REQUEST_URI'] ?? ''; - + . '' + . '' + . '
' - - - - - - - - \ No newline at end of file + . ''; +} \ No newline at end of file