diff --git a/auth/auth.php b/auth/auth.php index 1545d9f..bd3ca1d 100644 --- a/auth/auth.php +++ b/auth/auth.php @@ -1,261 +1,270 @@ 0, // 0 = cookie platí do zavření prohlížeče - // (trvalost zajišťuje remember me, ne session) + 'lifetime' => 0, 'path' => '/', - 'httponly' => true, // JavaScript k cookie nemá přístup - 'samesite' => 'Strict', // ochrana před CSRF - // 'secure' => true, // odkomentuj pokud web běží na HTTPS + 'httponly' => true, + 'samesite' => 'Strict', + // 'secure' => true, ]); session_start(); -// ------------------------------------------------------------ -// 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 = ''; - -// ------------------------------------------------------------ -// 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); +// 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); exit; } // ------------------------------------------------------------ -// PŘIPRAVENÝ HTML KÓD PRO ODHLAŠOVACÍ TLAČÍTKO +// CSRF TOKEN // ------------------------------------------------------------ -// Na chráněné stránce stačí napsat: -// -// echo $auth_logout_html; -// -// Pokud uživatel není přihlášen, proměnná je prázdný řetězec -// a nevypíše se nic. -if ($auth_prihlasen) { - $auth_logout_html = - '
' - . '' - . '' - . '
'; -} \ No newline at end of file +if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); +} +$csrf_token = $_SESSION['csrf_token']; + +// ------------------------------------------------------------ +// ZPRACOVÁNÍ FORMULÁŘE +// ------------------------------------------------------------ + +$chyba = ''; +$email_hodnota = ''; + +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; + } + } +} + +// ------------------------------------------------------------ +// HTML VÝSTUP +// ------------------------------------------------------------ +?> + + + + + Přihlášení – <?php echo htmlspecialchars(PROJEKT_NAZEV); ?> + + + + + + +

Přihlášení

+

+ + +

Chyba:

+ + +
+ + + +

+
+ +

+ +

+
+ +

+ +

+ +

+ +

+ +

+ +
+ + +

Nemáš účet? Zaregistruj se.

+ + +

Zapomněl jsi heslo?

+ + + \ No newline at end of file diff --git a/index.php b/index.php index a0bb0f2..20ffe87 100644 --- a/index.php +++ b/index.php @@ -1,12 +1,21 @@ - + -Chráněná stránka +Ukázka -

Přihlášen:

+ + + +

Přihlášen:

-

Zde je chráněný obsah.

+

Toto vidí pouze přihlášený uživatel.

+ + + +

Pro zobrazení obsahu se přihlas:

+ + + + \ No newline at end of file