[FÁZE-1][auth] Přidána přihlašovací stránka s CSRF ochranou a brute force kontrolou

This commit is contained in:
stepan
2026-03-16 23:47:46 +01:00
parent e0c5a25fdc
commit 0816e78049
+280
View File
@@ -0,0 +1,280 @@
<?php
// ============================================================
// PŘIHLAŠOVACÍ STRÁNKA
// ============================================================
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php';
// Spuštění session (stejné nastavení jako v auth.php)
session_name(SESSION_NAZEV);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
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);
exit;
}
// ------------------------------------------------------------
// CSRF TOKEN
// ------------------------------------------------------------
// CSRF token je náhodný řetězec, který se vygeneruje při načtení
// stránky a uloží do session. Formulář ho odešle jako skryté pole.
// Při zpracování formuláře ověříme, že token sedí tím zabráníme
// útoku, kdy by cizí web odeslal formulář jménem přihlášeného
// uživatele.
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
// ------------------------------------------------------------
// ZPRACOVÁNÍ FORMULÁŘE
// ------------------------------------------------------------
$chyba = ''; // chybová hláška pro uživatele
$email_hodnota = ''; // předvyplnění emailového pole po chybě
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// -- Ověření CSRF tokenu ----------------------------------
$csrf_z_formulare = $_POST['csrf_token'] ?? '';
// hash_equals porovnává řetězce v konstantním čase
// chrání před timing útoky na 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)) {
// -- Načtení hodnot z formuláře -----------------------
$email = trim($_POST['email'] ?? '');
$heslo = $_POST['heslo'] ?? '';
$zapamatovat = isset($_POST['zapamatovat']);
$email_hodnota = htmlspecialchars($email); // pro předvyplnění pole
// -- Základní validace --------------------------------
if (empty($email) || empty($heslo)) {
$chyba = 'Zadejte email a heslo.';
}
}
if (empty($chyba)) {
// -- Kontrola brute force -----------------------------
// Spočítáme neúspěšné pokusy z této IP adresy
// nebo pro tento email za poslední časové okno
$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) {
// Nezradíme přesný důvod jen obecná hláška
$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();
// Ověříme heslo pomocí password_verify().
// DŮLEŽITÉ: 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) {
// Přihlášení selhalo 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,
]);
// Obecná hláška neříkáme, zda byl špatný email nebo heslo
$chyba = 'Nesprávný email nebo heslo.';
} else {
// -- Přihlášení úspěšné ---------------------------
// Regenerujeme session ID ochrana před session fixation
// (útočník mohl podstrčit uživateli konkrétní session ID)
session_regenerate_id(true);
// Zapíšeme uživatele do session
$_SESSION['uzivatel_id'] = $uzivatel['id'];
$_SESSION['email'] = $uzivatel['email'];
$_SESSION['admin'] = (bool) $uzivatel['admin'];
$_SESSION['posledni_aktivita'] = time();
// Vygenerujeme nový CSRF token pro další požadavky
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// -- Remember me ----------------------------------
if ($zapamatovat) {
// Selector = veřejný identifikátor (jde do cookie i DB)
// Token = tajný řetězec (do cookie jde plaintext,
// do DB jen bcrypt hash)
$selector = bin2hex(random_bytes(16)); // 32 znaků hex
$token = bin2hex(random_bytes(32)); // 64 znaků hex
$token_hash = password_hash($token, PASSWORD_DEFAULT);
$expiruje = date('Y-m-d H:i:s', time() + REMEMBER_EXPIRACE);
// Uložíme token do DB
$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,
]);
// Nastavíme cookie v prohlížeči
// Cookie obsahuje POUZE selector a token žádné heslo,
// žádné ID, žádný příznak admin
$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í -------------------
// Pokud byl uživatel přesměrován z jiné stránky,
// vrátíme ho tam. Jinak jde na výchozí stránku.
$redirect = $_SESSION['redirect_po_prihlaseni'] ?? AUTH_REDIRECT_PO_PRIHLASENI;
unset($_SESSION['redirect_po_prihlaseni']);
header('Location: ' . $redirect);
exit;
}
}
}
// ------------------------------------------------------------
// HTML VÝSTUP
// ------------------------------------------------------------
?>
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Přihlášení <?php echo htmlspecialchars(PROJEKT_NAZEV); ?></title>
<?php if (AUTH_CSS !== ''): ?>
<link rel="stylesheet" href="<?php echo htmlspecialchars(AUTH_CSS); ?>">
<?php endif; ?>
</head>
<body>
<h1>Přihlášení</h1>
<h2><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></h2>
<?php if (!empty($chyba)): ?>
<p><strong>Chyba: <?php echo htmlspecialchars($chyba); ?></strong></p>
<?php endif; ?>
<form method="POST" action="">
<!-- CSRF token skryté pole, ověřuje se při odeslání formuláře -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<p>
<label for="email">Email:</label><br>
<input
type="email"
id="email"
name="email"
value="<?php echo $email_hodnota; ?>"
required
autocomplete="email"
>
</p>
<p>
<label for="heslo">Heslo:</label><br>
<input
type="password"
id="heslo"
name="heslo"
required
autocomplete="current-password"
>
</p>
<p>
<label>
<input type="checkbox" name="zapamatovat" value="1">
Zapamatovat si mě
</label>
</p>
<p>
<button type="submit">Přihlásit se</button>
</p>
</form>
<?php if (REGISTRACE_OTEVRENA): ?>
<p><a href="<?php echo htmlspecialchars(__DIR__); ?>/registrace.php">Nemáš účet? Zaregistruj se.</a></p>
<?php endif; ?>
<p><a href="<?php echo htmlspecialchars(__DIR__); ?>/reset_hesla.php">Zapomněl jsi heslo?</a></p>
</body>
</html>